Merge pull request #533 from LibreQoE/user_interface_2

User interface 2
This commit is contained in:
Robert Chacón 2024-07-23 14:59:04 -06:00 committed by GitHub
commit 35b436f48b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
535 changed files with 44121 additions and 9433 deletions

View File

@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo /bin/apt-get update; sudo /bin/apt-get install --no-install-recommends python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`/bin/uname r` libbpf-dev
run: sudo /bin/apt-get update; sudo /bin/apt-get install --no-install-recommends python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`/bin/uname r` libbpf-dev mold
if: matrix.os == 'ubuntu-latest'
- name: Build
run: pushd src/rust; cargo build --verbose --all; popd

2
.gitignore vendored
View File

@ -121,3 +121,5 @@ src/network.pdf
src/ShapedDevices.csv.good
.gitignore
src/rust/lqosd/lts_keys.bin
src/rust/lqosd/src/node_manager/js_build/out
src/bin/dashboards

View File

@ -27,17 +27,34 @@ from liblqos_python import is_lqosd_alive, clear_ip_mappings, delete_ip_mapping,
run_shell_commands_as_sudo, generated_pn_download_mbps, generated_pn_upload_mbps, queues_available_override, \
on_a_stick, get_tree_weights, get_weights
R2Q = 10
#MAX_R2Q = 200_000
MAX_R2Q = 60_000 # See https://lartc.vger.kernel.narkive.com/NKaH1ZNG/htb-quantum-of-class-100001-is-small-consider-r2q-change
MIN_QUANTUM = 1522
def calculateR2q(maxRateInMbps):
# So we've learned that r2q defaults to 10, and is used to calculate quantum. Quantum is rateInBytes/r2q by
# default. This default gives errors at high rates, and tc clamps the quantum to 200000. Setting a high quantum
# directly gives no errors. So we want to calculate r2q to default to 10, but not exceed 200000 for the highest
# specified rate (which will be the available bandwidth rate).
maxRateInBytesPerSecond = maxRateInMbps * 125000
r2q = 10
quantum = maxRateInBytesPerSecond / r2q
while quantum > MAX_R2Q:
r2q += 1
quantum = maxRateInBytesPerSecond / r2q
global R2Q
R2Q = r2q
def quantum(rateInMbps):
# Attempt to calculate an appropriate quantum for an HTB queue, given
# that `mq` does not appear to carry a valid `r2q` value to individual
# root nodes.
R2Q = 10
rateInBytesPerSecond = rateInMbps * 125000
quantum = int(rateInBytesPerSecond / R2Q)
quantum = max(MIN_QUANTUM, int(rateInBytesPerSecond / R2Q))
#print("R2Q=" + str(R2Q) + ", quantum: " + str(quantum))
quantrumString = " quantum " + str(quantum)
#print("Calculated quantum for " + str(rateInMbps) + " Mbps: " + str(quantum))
return quantrumString
#return " quantum 1522"
# Automatically account for TCP overhead of plans. For example a 100Mbps plan needs to be set to 109Mbps for the user to ever see that result on a speed test
# Does not apply to nodes of any sort, just endpoint devices
@ -793,6 +810,8 @@ def refreshShapers():
logging.info("# MQ Setup for " + thisInterface)
command = 'qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq'
linuxTCcommands.append(command)
maxBandwidth = max(upstream_bandwidth_capacity_upload_mbps(), upstream_bandwidth_capacity_download_mbps())
calculateR2q(maxBandwidth)
for queue in range(queuesAvailable):
command = 'qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2'
linuxTCcommands.append(command)

View File

@ -1,13 +0,0 @@
[Unit]
After=network.service lqosd.service
Requires=lqosd.service
[Service]
WorkingDirectory=/opt/libreqos/src/bin
ExecStart=/opt/libreqos/src/bin/lqos_node_manager
Restart=always
#Turn on debuging for service
#Environment=RUST_LOG=info
[Install]
WantedBy=default.target

View File

@ -21,8 +21,8 @@ LQOS_DIR=$DPKG_DIR/opt/libreqos/src
ETC_DIR=$DPKG_DIR/etc
MOTD_DIR=$DPKG_DIR/etc/update-motd.d
LQOS_FILES="graphInfluxDB.py influxDBdashboardTemplate.json integrationCommon.py integrationPowercode.py integrationRestHttp.py integrationSonar.py integrationSplynx.py integrationUISP.py integrationSonar.py LibreQoS.py lqos.example lqTools.py mikrotikFindIPv6.py network.example.json pythonCheck.py README.md scheduler.py ShapedDevices.example.csv lqos.example ../requirements.txt"
LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example lqos_node_manager.service.example"
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_setup lqos_map_perf uisp_integration lqos_support_tool"
LQOS_BIN_FILES="lqos_scheduler.service.example lqosd.service.example"
RUSTPROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqusers lqos_setup lqos_map_perf uisp_integration lqos_support_tool"
####################################################
# Clean any previous dist build
@ -39,12 +39,12 @@ mkdir -p $DEBIAN_DIR
# Build the chroot directory structure
mkdir -p $LQOS_DIR
mkdir -p $LQOS_DIR/bin/static
mkdir -p $LQOS_DIR/bin/static2
mkdir -p $ETC_DIR
mkdir -p $MOTD_DIR
# Create the Debian control file
pushd $DEBIAN_DIR > /dev/null
pushd $DEBIAN_DIR > /dev/null || exit
touch control
echo "Package: $PACKAGE" >> control
echo "Version: $VERSION" >> control
@ -52,10 +52,10 @@ echo "Architecture: amd64" >> control
echo "Maintainer: Herbert Wolverson <herberticus@gmail.com>" >> control
echo "Description: CAKE-based traffic shaping for ISPs" >> control
echo "Depends: $APT_DEPENDENCIES" >> control
popd > /dev/null
popd > /dev/null || exit
# Create the post-installation file
pushd $DEBIAN_DIR > /dev/null
pushd $DEBIAN_DIR > /dev/null || exit
touch postinst
echo "#!/bin/bash" >> postinst
echo "# Install Python Dependencies" >> postinst
@ -66,13 +66,13 @@ echo "sudo python3 -m pip install --break-system-packages -r src/requirements.tx
# - Run lqsetup
echo "/opt/libreqos/src/bin/lqos_setup" >> postinst
# - Setup the services
echo "cp /opt/libreqos/src/bin/lqos_node_manager.service.example /etc/systemd/system/lqos_node_manager.service" >> postinst
echo "cp /opt/libreqos/src/bin/lqosd.service.example /etc/systemd/system/lqosd.service" >> postinst
echo "cp /opt/libreqos/src/bin/lqos_scheduler.service.example /etc/systemd/system/lqos_scheduler.service" >> postinst
echo "/bin/systemctl daemon-reload" >> postinst
echo "/bin/systemctl enable lqosd lqos_node_manager lqos_scheduler" >> postinst
echo "/bin/systemctl stop lqos_node_manager" >> postinst # In case it's running from a previous release
echo "/bin/systemctl disable lqos_node_manager" >> postinst # In case it's running from a previous release
echo "/bin/systemctl enable lqosd lqos_scheduler" >> postinst
echo "/bin/systemctl start lqosd" >> postinst
echo "/bin/systemctl start lqos_node_manager" >> postinst
echo "/bin/systemctl start lqos_scheduler" >> postinst
echo "popd" >> postinst
# Attempting to fixup versioning issues with libpython.
@ -92,19 +92,18 @@ chmod a+x postinst
# Uninstall Script
touch postrm
echo "#!/bin/bash" >> postrm
echo "/bin/systemctl disable lqosd lqos_node_manager lqos_scheduler" >> postrm
echo "/bin/systemctl stop lqosd" >> postrm
echo "/bin/systemctl stop lqos_node_manager" >> postrm
echo "/bin/systemctl stop lqos_scheduler" >> postrm
echo "/bin/systemctl disable lqosd lqos_scheduler" >> postrm
chmod a+x postrm
popd > /dev/null
popd > /dev/null || exit
# Create the cleanup file
pushd $DEBIAN_DIR > /dev/null
pushd $DEBIAN_DIR > /dev/null || exit
touch postrm
echo "#!/bin/bash" >> postrm
chmod a+x postrm
popd > /dev/null
popd > /dev/null || exit
# Copy files into the LibreQoS directory
for file in $LQOS_FILES
@ -120,10 +119,10 @@ done
####################################################
# Build the Rust programs
pushd rust > /dev/null
pushd rust > /dev/null || exit
cargo clean
cargo build --all --release
popd > /dev/null
popd > /dev/null || exit
# Copy newly built Rust files
# - The Python integration Library
@ -133,13 +132,14 @@ for prog in $RUSTPROGS
do
cp rust/target/release/$prog $LQOS_DIR/bin
done
# - The webserver skeleton files
cp rust/lqos_node_manager/Rocket.toml $LQOS_DIR/bin
cp -R rust/lqos_node_manager/static/* $LQOS_DIR/bin/static
# Compile the website
pushd rust/lqosd > /dev/null || exit
./copy_files.sh
popd || exit
####################################################
# Add Message of the Day
pushd $MOTD_DIR > /dev/null
pushd $MOTD_DIR > /dev/null || exit
echo "#!/bin/bash" > 99-libreqos
echo "MY_IP=\'hostname -I | cut -d' ' -f1\'" >> 99-libreqos
echo "echo \"\"" >> 99-libreqos
@ -147,7 +147,7 @@ echo "echo \"LibreQoS Traffic Shaper is installed on this machine.\"" >> 99-libr
echo "echo \"Point a browser at http://\$MY_IP:9123/ to manage it.\"" >> 99-libreqos
echo "echo \"\"" >> 99-libreqos
chmod a+x 99-libreqos
popd
popd || exit
####################################################
# Assemble the package

View File

@ -3,13 +3,12 @@
# This script builds the Rust sub-system and places the results in the
# `src/bin` directory.
#
# You still need to setup services to run `lqosd` and `lqos_node_manager`
# automatically.
# You still need to setup services to run `lqosd` and possibly `lqos_scheduler` automatically.
#
# Don't forget to setup `/etc/lqos.conf`
# Check Pre-Requisites
sudo apt install python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`uname -r` libbpf-dev libssl-dev
sudo apt install python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`uname -r` libbpf-dev libssl-dev esbuild mold
if ! rustup -V &> /dev/null
then
@ -39,7 +38,7 @@ rustup update
# Start building
echo "Please wait while the system is compiled. Service will not be interrupted during this stage."
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager lqusers lqos_map_perf uisp_integration lqos_support_tool"
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqusers lqos_map_perf uisp_integration lqos_support_tool"
mkdir -p bin/static
pushd rust > /dev/null
#cargo clean
@ -65,10 +64,10 @@ done
popd > /dev/null
# Copy the node manager's static web content
cp -R rust/lqos_node_manager/static/* bin/static
# Copy Rocket.toml to tell the node manager where to listen
cp rust/lqos_node_manager/Rocket.toml bin/
mkdir -p bin/static2/vendor
pushd rust/lqosd > /dev/null
./copy_files.sh
popd > /dev/null
# Copy the Python library for LibreQoS.py et al.
pushd rust/lqos_python > /dev/null
@ -87,6 +86,11 @@ service_exists() {
fi
}
if service_exists lqos_node_manager; then
echo "lqos_node_manager is running as a service. It's not needed anymore. Killing it."
sudo systemctl stop lqos_node_manager
sudo systemctl disable lqos_node_manager
fi
if service_exists lqosd; then
echo "lqosd is running as a service. Restarting it. You may need to enter your sudo password."
sudo systemctl restart lqosd
@ -95,10 +99,6 @@ if service_exists lqos_scheduler; then
echo "lqos_scheduler is running as a service. Restarting it. You may need to enter your sudo password."
sudo systemctl restart lqos_scheduler
fi
if service_exists lqos_node_manager; then
echo "lqos_node_manager is running as a service. Restarting it. You may need to enter your sudo password."
sudo systemctl restart lqos_node_manager
fi
echo "-----------------------------------------------------------------"
echo "Don't forget to setup /etc/lqos.conf!"

View File

@ -88,6 +88,7 @@ bandwidth_overhead_factor = 1.0
commit_bandwidth_multiplier = 0.98
exception_cpes = []
use_ptmp_as_parent = false
ignore_calculated_capacity = false
[powercode_integration]
enable_powercode = false

View File

@ -0,0 +1,3 @@
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]

1312
src/rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,14 @@ version = "0.1.0"
edition = "2021"
license = "GPL-2.0-only"
[dependencies]
[profile.release]
strip = "debuginfo"
lto = "fat"
lto = "thin"
incremental = true
[profile.release.build-override]
opt-level = 3
codegen-units = 16
[workspace]
members = [
@ -19,7 +22,6 @@ members = [
"lqtop", # A command line utility to show current activity
"xdp_iphash_to_cpu_cmdline", # Rust port of the C xdp_iphash_to_cpu_cmdline tool, for compatibility
"xdp_pping", # Rust port of cpumap's `xdp_pping` tool, for compatibility
"lqos_node_manager", # A lightweight web interface for management and local monitoring
"lqos_python", # Python bindings for using the Rust bus directly
"lqusers", # CLI control for managing the web user list
"lqos_utils", # A collection of macros and helpers we find useful
@ -34,3 +36,37 @@ members = [
"uisp_integration", # UISP Integration in Rust
"lqos_support_tool", # A Helper tool to make it easier to request/receive support
]
[dependencies]
[workspace.dependencies]
anyhow = "1"
thiserror = "1"
tokio = { version = "1", features = [ "full" ] }
log = "0"
bincode = "1"
once_cell = "1"
nix = { version = "0", features = ["time"] }
serde = { version = "1.0", features = ["derive"] }
serde_cbor = "0" # For RFC8949/7409 format C binary objects
serde_json = "1"
csv = "1"
ip_network_table = "0"
ip_network = "0"
sha2 = "0"
uuid = { version = "1", features = ["v4", "fast-rng" ] }
dashmap = "5"
toml = "0.8.8"
zerocopy = {version = "0.6.1", features = [ "simd" ] }
sysinfo = "0"
default-net = "0"
reqwest = { version = "0", features = ["json", "blocking"] }
pyo3 = "0.20.3"
colored = "2"
miniz_oxide = "0.7"
byteorder = "1"
num-traits = "0.2.19"
clap = { version = "4", features = ["derive"] }
# May have to change this one for ARM?
jemallocator = "0.5"

View File

@ -6,11 +6,10 @@ license = "GPL-2.0-only"
[dependencies]
tokio = { version = "1.25.0", features = ["full"] }
anyhow = "1"
anyhow = { workspace = true }
env_logger = "0"
log = "0"
log = { workspace = true }
lqos_bus = { path = "../lqos_bus" }
serde = { version = "1.0", features = ["derive"] }
serde_cbor = "0" # For RFC8949/7409 format C binary objects
serde_cbor = { workspace = true }
sqlite = "0.30.4"
axum = "0.6"

View File

@ -9,16 +9,16 @@ default = ["equinix_tests"]
equinix_tests = []
[dependencies]
serde = { version = "1.0", features = ["derive"] }
bincode = "1"
thiserror = "1"
serde = { workspace = true }
bincode = { workspace = true }
thiserror = { workspace = true }
lqos_config = { path = "../lqos_config" }
lqos_utils = { path = "../lqos_utils" }
lts_client = { path = "../lts_client" }
tokio = { version = "1", features = [ "full" ] }
log = "0"
nix = "0"
serde_cbor = "0" # For RFC8949/7409 format C binary objects
tokio = { workspace = true }
log = { workspace = true }
nix = { workspace = true }
serde_cbor = { workspace = true }
[dev-dependencies]
criterion = { version = "0", features = [ "html_reports", "async_tokio"] }

View File

@ -5,6 +5,7 @@ use crate::{
use lts_client::transport_data::{StatsTotals, StatsHost, StatsTreeNode};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use lqos_utils::units::DownUpOrder;
/// A `BusResponse` object represents a single
/// reply generated from a `BusRequest`, and batched
@ -24,18 +25,18 @@ pub enum BusResponse {
/// Current throughput for the overall system.
CurrentThroughput {
/// In bps
bits_per_second: (u64, u64),
bits_per_second: DownUpOrder<u64>,
/// In pps
packets_per_second: (u64, u64),
packets_per_second: DownUpOrder<u64>,
/// How much of the response has been subject to the shaper?
shaped_bits_per_second: (u64, u64),
shaped_bits_per_second: DownUpOrder<u64>,
},
/// Provides a list of ALL mapped hosts traffic counters,
/// listing the IP Address and upload/download in a tuple.
HostCounters(Vec<(IpAddr, u64, u64)>),
HostCounters(Vec<(IpAddr, DownUpOrder<u64>)>),
/// Provides the Top N downloaders IP stats.
TopDownloaders(Vec<IpStats>),
@ -89,7 +90,7 @@ pub enum BusResponse {
/// Us to poll hosts
time_to_poll_hosts: u64,
/// High traffic watermark
high_watermark: (u64, u64),
high_watermark: DownUpOrder<u64>,
/// Number of flows tracked
tracked_flows: u64,
/// RTT events per second
@ -132,7 +133,7 @@ pub enum BusResponse {
FlowsByIp(Vec<FlowbeeSummaryData>),
/// Current endpoints by country
CurrentEndpointsByCountry(Vec<(String, [u64; 2], [f32; 2])>),
CurrentEndpointsByCountry(Vec<(String, DownUpOrder<u64>, [f32; 2], String)>),
/// Current Lat/Lon of endpoints
CurrentLatLon(Vec<(f64, f64, String, u64, f32)>),
@ -140,19 +141,19 @@ pub enum BusResponse {
/// Summary of Ether Protocol
EtherProtocols{
/// Number of IPv4 Bytes
v4_bytes: [u64; 2],
v4_bytes: DownUpOrder<u64>,
/// Number of IPv6 Bytes
v6_bytes: [u64; 2],
v6_bytes: DownUpOrder<u64>,
/// Number of IPv4 Packets
v4_packets: [u64; 2],
v4_packets: DownUpOrder<u64>,
/// Number of IPv6 Packets
v6_packets: [u64; 2],
v6_packets: DownUpOrder<u64>,
/// Number of IPv4 Flows
v4_rtt: [u64; 2],
v4_rtt: DownUpOrder<u64>,
/// Number of IPv6 Flows
v6_rtt: [u64; 2],
v6_rtt: DownUpOrder<u64>,
},
/// Summary of IP Protocols
IpProtocols(Vec<(String, (u64, u64))>),
IpProtocols(Vec<(String, DownUpOrder<u64>)>),
}

View File

@ -1,5 +1,6 @@
use crate::TcHandle;
use serde::{Deserialize, Serialize};
use lqos_utils::units::DownUpOrder;
/// Transmission representation of IP statistics associated
/// with a host.
@ -11,13 +12,12 @@ pub struct IpStats {
/// The host's mapped circuit ID
pub circuit_id: String,
/// The current bits-per-second passing through this host. Tuple
/// 0 is download, tuple 1 is upload.
pub bits_per_second: (u64, u64),
/// The current bits-per-second passing through this host.
pub bits_per_second: DownUpOrder<u64>,
/// The current packets-per-second passing through this host. Tuple
/// 0 is download, tuple 1 is upload.
pub packets_per_second: (u64, u64),
pub packets_per_second: DownUpOrder<u64>,
/// Median TCP round-trip-time for this host at the current time.
pub median_tcp_rtt: f32,
@ -26,7 +26,7 @@ pub struct IpStats {
pub tc_handle: TcHandle,
/// TCP Retransmits for this host at the current time.
pub tcp_retransmits: (u64, u64),
pub tcp_retransmits: DownUpOrder<u64>,
}
/// Represents an IP Mapping in the XDP IP to TC/CPU mapping system.
@ -153,13 +153,13 @@ pub struct FlowbeeSummaryData {
/// Time (nanos) when the connection was last seen
pub last_seen: u64,
/// Bytes transmitted
pub bytes_sent: [u64; 2],
pub bytes_sent: DownUpOrder<u64>,
/// Packets transmitted
pub packets_sent: [u64; 2],
pub packets_sent: DownUpOrder<u64>,
/// Rate estimate
pub rate_estimate_bps: [u32; 2],
pub rate_estimate_bps: DownUpOrder<u32>,
/// TCP Retransmission count (also counts duplicates)
pub tcp_retransmits: [u16; 2],
pub tcp_retransmits: DownUpOrder<u16>,
/// Has the connection ended?
/// 0 = Alive, 1 = FIN, 2 = RST
pub end_status: u8,
@ -168,7 +168,7 @@ pub struct FlowbeeSummaryData {
/// Raw TCP flags
pub flags: u8,
/// Recent RTT median
pub rtt_nanos: [u64; 2],
pub rtt_nanos: DownUpOrder<u64>,
/// Remote ASN
pub remote_asn: u32,
/// Remote ASN Name
@ -177,4 +177,8 @@ pub struct FlowbeeSummaryData {
pub remote_asn_country: String,
/// Analysis
pub analysis: String,
/// Circuit ID
pub circuit_id: String,
/// Circuit Name
pub circuit_name: String,
}

View File

@ -5,17 +5,17 @@ edition = "2021"
license = "GPL-2.0-only"
[dependencies]
thiserror = "1"
thiserror = { workspace = true }
toml_edit = { version = "0", features = [ "serde" ] }
serde = { version = "1.0", features = [ "derive" ] }
serde_json = "1"
csv = "1"
ip_network_table = "0"
ip_network = "0"
sha2 = "0"
uuid = { version = "1", features = ["v4", "fast-rng" ] }
log = "0"
dashmap = "5"
pyo3 = "0.20"
toml = "0.8.8"
once_cell = "1.19.0"
serde = { workspace = true }
serde_json = { workspace = true }
csv = { workspace = true }
ip_network_table = { workspace = true }
ip_network = { workspace = true }
sha2 = { workspace = true }
uuid = { workspace = true }
log = { workspace = true }
dashmap = { workspace = true }
pyo3 = { workspace = true }
toml = { workspace = true }
lqos_utils = { path = "../lqos_utils" }

View File

@ -66,6 +66,11 @@ impl WebUsers {
Ok(filename)
}
/// Is the list of users empty?
pub fn is_empty(&self) -> bool {
self.users.is_empty()
}
fn save_to_disk(&self) -> Result<(), AuthenticationError> {
let path = Self::path()?;
let new_contents = toml_edit::ser::to_string(&self);

View File

@ -1,7 +1,7 @@
//! Manages the `/etc/lqos.conf` file.
use log::error;
use serde::{Deserialize, Serialize};
use toml_edit::{Document, value};
use toml_edit::{DocumentMut, value};
use std::{fs, path::Path};
use thiserror::Error;
@ -172,7 +172,7 @@ impl EtcLqos {
pub(crate) fn load_from_string(raw: &str) -> Result<Self, EtcLqosError> {
log::info!("Trying to load old TOML version from /etc/lqos.conf");
let document = raw.parse::<Document>();
let document = raw.parse::<DocumentMut>();
match document {
Err(e) => {
error!("Unable to parse TOML from /etc/lqos.conf");
@ -199,7 +199,7 @@ impl EtcLqos {
/// Saves changes made to /etc/lqos.conf
/// Copies current configuration into /etc/lqos.conf.backup first
pub fn save(&self, document: &mut Document) -> Result<(), EtcLqosError> {
pub fn save(&self, document: &mut DocumentMut) -> Result<(), EtcLqosError> {
let cfg_path = Path::new("/etc/lqos.conf");
let backup_path = Path::new("/etc/lqos.conf.backup");
if let Err(e) = std::fs::copy(cfg_path, backup_path) {
@ -223,7 +223,7 @@ impl EtcLqos {
#[allow(dead_code)]
pub fn enable_long_term_stats(license_key: String) {
if let Ok(raw) = std::fs::read_to_string("/etc/lqos.conf") {
let document = raw.parse::<Document>();
let document = raw.parse::<DocumentMut>();
match document {
Err(e) => {
error!("Unable to parse TOML from /etc/lqos.conf");
@ -267,7 +267,7 @@ pub fn enable_long_term_stats(license_key: String) {
}
}
fn check_config(cfg_doc: &mut Document, cfg: &mut EtcLqos) {
fn check_config(cfg_doc: &mut DocumentMut, cfg: &mut EtcLqos) {
use sha2::digest::Update;
use sha2::Digest;
@ -307,14 +307,14 @@ mod test {
#[test]
fn round_trip_toml() {
let doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::Document>().unwrap();
let doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::DocumentMut>().unwrap();
let reserialized = doc.to_string();
assert_eq!(EXAMPLE_LQOS_CONF.trim(), reserialized.trim());
}
#[test]
fn add_node_id() {
let mut doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::Document>().unwrap();
let mut doc = EXAMPLE_LQOS_CONF.parse::<toml_edit::DocumentMut>().unwrap();
doc["node_id"] = toml_edit::value("test");
let reserialized = doc.to_string();
assert!(reserialized.contains("node_id = \"test\""));

View File

@ -6,7 +6,7 @@ use super::{
EtcLqosError, EtcLqos,
};
use thiserror::Error;
use toml_edit::Document;
use toml_edit::DocumentMut;
#[derive(Debug, Error)]
pub enum MigrationError {
@ -28,7 +28,7 @@ pub fn migrate_if_needed() -> Result<(), MigrationError> {
std::fs::read_to_string("/etc/lqos.conf").map_err(|e| MigrationError::ReadError(e))?;
let doc = raw
.parse::<Document>()
.parse::<DocumentMut>()
.map_err(|e| MigrationError::ParseError(e))?;
if let Some((_key, version)) = doc.get_key_value("version") {
log::info!("Configuration file is at version {}", version.as_str().unwrap());

View File

@ -16,6 +16,12 @@ pub struct UispIntegration {
pub commit_bandwidth_multiplier: f32,
pub exception_cpes: Vec<ExceptionCpe>,
pub use_ptmp_as_parent: bool,
#[serde(default = "default_ignore_calculated_capacity")]
pub ignore_calculated_capacity: bool,
}
fn default_ignore_calculated_capacity() -> bool {
false
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@ -41,6 +47,7 @@ impl Default for UispIntegration {
commit_bandwidth_multiplier: 1.0,
exception_cpes: vec![],
use_ptmp_as_parent: false,
ignore_calculated_capacity: false,
}
}
}

View File

@ -14,7 +14,7 @@ mod shaped_devices;
pub use authentication::{UserRole, WebUsers};
pub use etc::{load_config, Config, enable_long_term_stats, Tunables, BridgeConfig, update_config, disable_xdp_bridge};
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport};
pub use network_json::{NetworkJson, NetworkJsonNode, NetworkJsonTransport, NetworkJsonCounting};
pub use program_control::load_libreqos;
pub use shaped_devices::{ConfigShapedDevices, ShapedDevice};

View File

@ -0,0 +1,226 @@
mod network_json_node;
mod network_json_transport;
mod network_json_counting;
use dashmap::DashSet;
use log::{error, info};
use serde_json::{Map, Value};
use std::{
fs, path::{Path, PathBuf},
};
use thiserror::Error;
use lqos_utils::units::DownUpOrder;
pub use network_json_node::NetworkJsonNode;
pub use network_json_transport::NetworkJsonTransport;
pub use network_json_counting::NetworkJsonCounting;
/// Holder for the network.json representation.
/// This is condensed into a single level vector with index-based referencing
/// for easy use in funnel calculations.
#[derive(Debug)]
pub struct NetworkJson {
/// Nodes that make up the tree, flattened and referenced by index number.
/// TODO: We should add a primary key to nodes in network.json.
nodes: Vec<NetworkJsonNode>,
}
impl Default for NetworkJson {
fn default() -> Self {
Self::new()
}
}
impl NetworkJson {
/// Generates an empty network.json
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
/// The path to the current `network.json` file, determined
/// by acquiring the prefix from the `/etc/lqos.conf` configuration
/// file.
pub fn path() -> Result<PathBuf, NetworkJsonError> {
let cfg =
crate::load_config().map_err(|_| NetworkJsonError::ConfigLoadError)?;
let base_path = Path::new(&cfg.lqos_directory);
Ok(base_path.join("network.json"))
}
/// Does network.json exist?
pub fn exists() -> bool {
if let Ok(path) = Self::path() {
path.exists()
} else {
false
}
}
/// Attempt to load network.json from disk
pub fn load() -> Result<Self, NetworkJsonError> {
let mut nodes = vec![NetworkJsonNode {
name: "Root".to_string(),
max_throughput: (0, 0),
current_throughput: DownUpOrder::zeroed(),
current_tcp_retransmits: DownUpOrder::zeroed(),
current_drops: DownUpOrder::zeroed(),
current_marks: DownUpOrder::zeroed(),
parents: Vec::new(),
immediate_parent: None,
rtts: DashSet::new(),
node_type: None,
}];
if !Self::exists() {
return Err(NetworkJsonError::FileNotFound);
}
let path = Self::path()?;
let raw = fs::read_to_string(path)
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
let json: Value = serde_json::from_str(&raw)
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
// Start reading from the top. We are at the root node.
let parents = vec![0];
if let Value::Object(map) = &json {
for (key, value) in map.iter() {
if let Value::Object(inner_map) = value {
recurse_node(&mut nodes, key, inner_map, &parents, 0);
}
}
}
Ok(Self { nodes })
}
/// Find the index of a circuit_id
pub fn get_index_for_name(&self, name: &str) -> Option<usize> {
self.nodes.iter().position(|n| n.name == name)
}
/// Retrieve a cloned copy of a NetworkJsonNode entry, or None if there isn't
/// an entry at that index.
pub fn get_cloned_entry_by_index(
&self,
index: usize,
) -> Option<NetworkJsonTransport> {
self.nodes.get(index).map(|n| n.clone_to_transit())
}
/// Retrieve a cloned copy of all children with a parent containing a specific
/// node index.
pub fn get_cloned_children(
&self,
index: usize,
) -> Vec<(usize, NetworkJsonTransport)> {
self
.nodes
.iter()
.enumerate()
.filter(|(_i, n)| n.immediate_parent == Some(index))
.map(|(i, n)| (i, n.clone_to_transit()))
.collect()
}
/// Find a circuit_id, and if it exists return its list of parent nodes
/// as indices within the network_json layout.
pub fn get_parents_for_circuit_id(
&self,
circuit_id: &str,
) -> Option<Vec<usize>> {
//println!("Looking for parents of {circuit_id}");
self
.nodes
.iter()
.find(|n| n.name == circuit_id)
.map(|node| node.parents.clone())
}
/// Obtains a reference to nodes once we're sure that
/// doing so will provide valid data.
pub fn get_nodes_when_ready(&self) -> &Vec<NetworkJsonNode> {
//log::warn!("Awaiting the network tree");
//atomic_wait::wait(&self.busy, 1);
//log::warn!("Acquired");
&self.nodes
}
/// Starts an update cycle. This clones the nodes into
/// another structure - work will be performed on the clone.
pub fn begin_update_cycle(&self) -> NetworkJsonCounting {
NetworkJsonCounting::begin_update_cycle(self.nodes.clone())
}
/// Finishes an update cycle. This is called after all updates
/// have been made to the clone, and the clone is then copied back
/// into the main structure.
pub fn finish_update_cycle(&mut self, counting: NetworkJsonCounting) {
if !counting.nodes.is_empty() {
self.nodes = counting.nodes;
}
}
}
fn json_to_u32(val: Option<&Value>) -> u32 {
if let Some(val) = val {
if let Some(n) = val.as_u64() {
n as u32
} else {
0
}
} else {
0
}
}
fn recurse_node(
nodes: &mut Vec<NetworkJsonNode>,
name: &str,
json: &Map<String, Value>,
parents: &[usize],
immediate_parent: usize,
) {
info!("Mapping {name} from network.json");
let mut parents = parents.to_vec();
let my_id = if name != "children" {
parents.push(nodes.len());
nodes.len()
} else {
nodes.len() - 1
};
let node = NetworkJsonNode {
parents: parents.to_vec(),
max_throughput: (
json_to_u32(json.get("downloadBandwidthMbps")),
json_to_u32(json.get("uploadBandwidthMbps")),
),
current_throughput: DownUpOrder::zeroed(),
current_tcp_retransmits: DownUpOrder::zeroed(),
current_drops: DownUpOrder::zeroed(),
current_marks: DownUpOrder::zeroed(),
name: name.to_string(),
immediate_parent: Some(immediate_parent),
rtts: DashSet::new(),
node_type: json.get("type").map(|v| v.as_str().unwrap().to_string()),
};
if node.name != "children" {
nodes.push(node);
}
// Recurse children
for (key, value) in json.iter() {
let key_str = key.as_str();
if key_str != "uploadBandwidthMbps" && key_str != "downloadBandwidthMbps" {
if let Value::Object(value) = value {
recurse_node(nodes, key, value, &parents, my_id);
}
}
}
}
#[derive(Error, Debug)]
pub enum NetworkJsonError {
#[error("Unable to find or load network.json")]
ConfigLoadError,
#[error("network.json not found or does not exist")]
FileNotFound,
}

View File

@ -1,302 +0,0 @@
use dashmap::DashSet;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::{
fs,
path::{Path, PathBuf}, sync::atomic::AtomicU64,
};
use thiserror::Error;
/// Describes a node in the network map tree.
#[derive(Debug)]
pub struct NetworkJsonNode {
/// The node name, as it appears in `network.json`
pub name: String,
/// The maximum throughput allowed per `network.json` for this node
pub max_throughput: (u32, u32), // In mbps
/// Current throughput (in bytes/second) at this node
pub current_throughput: (AtomicU64, AtomicU64), // In bytes
/// Approximate RTTs reported for this level of the tree.
/// It's never going to be as statistically accurate as the actual
/// numbers, being based on medians.
pub rtts: DashSet<u16>,
/// A list of indices in the `NetworkJson` vector of nodes
/// linking to parent nodes
pub parents: Vec<usize>,
/// The immediate parent node
pub immediate_parent: Option<usize>,
/// The node type
pub node_type: Option<String>,
}
impl NetworkJsonNode {
/// Make a deep copy of a `NetworkJsonNode`, converting atomics
/// into concrete values.
pub fn clone_to_transit(&self) -> NetworkJsonTransport {
NetworkJsonTransport {
name: self.name.clone(),
max_throughput: self.max_throughput,
current_throughput: (
self.current_throughput.0.load(std::sync::atomic::Ordering::Relaxed),
self.current_throughput.1.load(std::sync::atomic::Ordering::Relaxed),
),
rtts: self.rtts.iter().map(|n| *n as f32 / 100.0).collect(),
parents: self.parents.clone(),
immediate_parent: self.immediate_parent,
node_type: self.node_type.clone(),
}
}
}
/// A "transport-friendly" version of `NetworkJsonNode`. Designed
/// to be quickly cloned from original nodes and efficiently
/// transmitted/received.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkJsonTransport {
/// Display name
pub name: String,
/// Max throughput for node (not clamped)
pub max_throughput: (u32, u32),
/// Current node throughput
pub current_throughput: (u64, u64),
/// Set of RTT data
pub rtts: Vec<f32>,
/// Node indices of parents
pub parents: Vec<usize>,
/// The immediate parent node in the tree
pub immediate_parent: Option<usize>,
/// The type of node (site, ap, etc.)
#[serde(rename = "type")]
pub node_type: Option<String>,
}
/// Holder for the network.json representation.
/// This is condensed into a single level vector with index-based referencing
/// for easy use in funnel calculations.
#[derive(Debug)]
pub struct NetworkJson {
/// Nodes that make up the tree, flattened and referenced by index number.
/// TODO: We should add a primary key to nodes in network.json.
pub nodes: Vec<NetworkJsonNode>,
}
impl Default for NetworkJson {
fn default() -> Self {
Self::new()
}
}
impl NetworkJson {
/// Generates an empty network.json
pub fn new() -> Self {
Self { nodes: Vec::new() }
}
/// The path to the current `network.json` file, determined
/// by acquiring the prefix from the `/etc/lqos.conf` configuration
/// file.
pub fn path() -> Result<PathBuf, NetworkJsonError> {
let cfg =
crate::load_config().map_err(|_| NetworkJsonError::ConfigLoadError)?;
let base_path = Path::new(&cfg.lqos_directory);
Ok(base_path.join("network.json"))
}
/// Does network.json exist?
pub fn exists() -> bool {
if let Ok(path) = Self::path() {
path.exists()
} else {
false
}
}
/// Attempt to load network.json from disk
pub fn load() -> Result<Self, NetworkJsonError> {
let mut nodes = vec![NetworkJsonNode {
name: "Root".to_string(),
max_throughput: (0, 0),
current_throughput: (AtomicU64::new(0), AtomicU64::new(0)),
parents: Vec::new(),
immediate_parent: None,
rtts: DashSet::new(),
node_type: None,
}];
if !Self::exists() {
return Err(NetworkJsonError::FileNotFound);
}
let path = Self::path()?;
let raw = fs::read_to_string(path)
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
let json: Value = serde_json::from_str(&raw)
.map_err(|_| NetworkJsonError::ConfigLoadError)?;
// Start reading from the top. We are at the root node.
let parents = vec![0];
if let Value::Object(map) = &json {
for (key, value) in map.iter() {
if let Value::Object(inner_map) = value {
recurse_node(&mut nodes, key, inner_map, &parents, 0);
}
}
}
Ok(Self { nodes })
}
/// Find the index of a circuit_id
pub fn get_index_for_name(&self, name: &str) -> Option<usize> {
self.nodes.iter().position(|n| n.name == name)
}
/// Retrieve a cloned copy of a NetworkJsonNode entry, or None if there isn't
/// an entry at that index.
pub fn get_cloned_entry_by_index(
&self,
index: usize,
) -> Option<NetworkJsonTransport> {
self.nodes.get(index).map(|n| n.clone_to_transit())
}
/// Retrieve a cloned copy of all children with a parent containing a specific
/// node index.
pub fn get_cloned_children(
&self,
index: usize,
) -> Vec<(usize, NetworkJsonTransport)> {
self
.nodes
.iter()
.enumerate()
.filter(|(_i, n)| n.immediate_parent == Some(index))
.map(|(i, n)| (i, n.clone_to_transit()))
.collect()
}
/// Find a circuit_id, and if it exists return its list of parent nodes
/// as indices within the network_json layout.
pub fn get_parents_for_circuit_id(
&self,
circuit_id: &str,
) -> Option<Vec<usize>> {
//println!("Looking for parents of {circuit_id}");
self
.nodes
.iter()
.find(|n| n.name == circuit_id)
.map(|node| node.parents.clone())
}
/// Sets all current throughput values to zero
/// Note that due to interior mutability, this does not require mutable
/// access.
pub fn zero_throughput_and_rtt(&self) {
self.nodes.iter().for_each(|n| {
n.current_throughput.0.store(0, std::sync::atomic::Ordering::Relaxed);
n.current_throughput.1.store(0, std::sync::atomic::Ordering::Relaxed);
n.rtts.clear();
});
}
/// Add throughput numbers to node entries. Note that this does *not* require
/// mutable access due to atomics and interior mutability - so it is safe to use
/// a read lock.
pub fn add_throughput_cycle(
&self,
targets: &[usize],
bytes: (u64, u64),
) {
for idx in targets {
// Safety first: use "get" to ensure that the node exists
if let Some(node) = self.nodes.get(*idx) {
node.current_throughput.0.fetch_add(bytes.0, std::sync::atomic::Ordering::Relaxed);
node.current_throughput.1.fetch_add(bytes.1, std::sync::atomic::Ordering::Relaxed);
} else {
warn!("No network tree entry for index {idx}");
}
}
}
/// Record RTT time in the tree. Note that due to interior mutability,
/// this does not require mutable access.
pub fn add_rtt_cycle(&self, targets: &[usize], rtt: f32) {
for idx in targets {
// Safety first: use "get" to ensure that the node exists
if let Some(node) = self.nodes.get(*idx) {
node.rtts.insert((rtt * 100.0) as u16);
} else {
warn!("No network tree entry for index {idx}");
}
}
}
}
fn json_to_u32(val: Option<&Value>) -> u32 {
if let Some(val) = val {
if let Some(n) = val.as_u64() {
n as u32
} else {
0
}
} else {
0
}
}
fn recurse_node(
nodes: &mut Vec<NetworkJsonNode>,
name: &str,
json: &Map<String, Value>,
parents: &[usize],
immediate_parent: usize,
) {
info!("Mapping {name} from network.json");
let mut parents = parents.to_vec();
let my_id = if name != "children" {
parents.push(nodes.len());
nodes.len()
} else {
nodes.len() - 1
};
let node = NetworkJsonNode {
parents: parents.to_vec(),
max_throughput: (
json_to_u32(json.get("downloadBandwidthMbps")),
json_to_u32(json.get("uploadBandwidthMbps")),
),
current_throughput: (AtomicU64::new(0), AtomicU64::new(0)),
name: name.to_string(),
immediate_parent: Some(immediate_parent),
rtts: DashSet::new(),
node_type: json.get("type").map(|v| v.as_str().unwrap().to_string()),
};
if node.name != "children" {
nodes.push(node);
}
// Recurse children
for (key, value) in json.iter() {
let key_str = key.as_str();
if key_str != "uploadBandwidthMbps" && key_str != "downloadBandwidthMbps" {
if let Value::Object(value) = value {
recurse_node(nodes, key, value, &parents, my_id);
}
}
}
}
#[derive(Error, Debug)]
pub enum NetworkJsonError {
#[error("Unable to find or load network.json")]
ConfigLoadError,
#[error("network.json not found or does not exist")]
FileNotFound,
}

View File

@ -0,0 +1,91 @@
use log::warn;
use lqos_utils::units::DownUpOrder;
use crate::NetworkJsonNode;
/// Type used while updating the network tree with new data.
/// Rather than have a race condition while the updates are performed
/// (and potentially new requests come in, and receive invalid data),
/// we copy the network tree into this structure, and then update this
/// structure. Once the updates are complete, we copy the data back
/// into the main network tree.
pub struct NetworkJsonCounting {
pub(super) nodes: Vec<NetworkJsonNode>,
}
impl NetworkJsonCounting {
/// Starts an update cycle. This clones the nodes into
/// the `NetworkJsonCounting` structure - work will be performed on the clone.
pub fn begin_update_cycle(nodes: Vec<NetworkJsonNode>) -> Self {
Self { nodes }
}
/// Sets all current throughput values to zero
/// Note that due to interior mutability, this does not require mutable
/// access.
pub fn zero_throughput_and_rtt(&mut self) {
//log::warn!("Locking network tree for throughput cycle");
self.nodes.iter_mut().for_each(|n| {
n.current_throughput.set_to_zero();
n.current_tcp_retransmits.set_to_zero();
n.rtts.clear();
n.current_drops.set_to_zero();
n.current_marks.set_to_zero();
});
}
/// Add throughput numbers to node entries. Note that this does *not* require
/// mutable access due to atomics and interior mutability - so it is safe to use
/// a read lock.
pub fn add_throughput_cycle(
&mut self,
targets: &[usize],
bytes: (u64, u64),
) {
for idx in targets {
// Safety first: use "get" to ensure that the node exists
if let Some(node) = self.nodes.get_mut(*idx) {
node.current_throughput.checked_add_tuple(bytes);
} else {
warn!("No network tree entry for index {idx}");
}
}
}
/// Record RTT time in the tree. Note that due to interior mutability,
/// this does not require mutable access.
pub fn add_rtt_cycle(&self, targets: &[usize], rtt: f32) {
for idx in targets {
// Safety first: use "get" to ensure that the node exists
if let Some(node) = self.nodes.get(*idx) {
node.rtts.insert((rtt * 100.0) as u16);
} else {
warn!("No network tree entry for index {idx}");
}
}
}
/// Record TCP Retransmits in the tree.
pub fn add_retransmit_cycle(&mut self, targets: &[usize], tcp_retransmits: DownUpOrder<u64>) {
for idx in targets {
// Safety first; use "get" to ensure that the node exists
if let Some(node) = self.nodes.get_mut(*idx) {
node.current_tcp_retransmits.checked_add(tcp_retransmits);
} else {
warn!("No network tree entry for index {idx}");
}
}
}
/// Adds a series of CAKE marks and drops to the tree structure.
pub fn add_queue_cycle(&mut self, targets: &[usize], marks: &DownUpOrder<u64>, drops: &DownUpOrder<u64>) {
for idx in targets {
// Safety first; use "get" to ensure that the node exists
if let Some(node) = self.nodes.get_mut(*idx) {
node.current_marks.checked_add(*marks);
node.current_drops.checked_add(*drops);
} else {
warn!("No network tree entry for index {idx}");
}
}
}
}

View File

@ -0,0 +1,71 @@
use dashmap::DashSet;
use lqos_utils::units::DownUpOrder;
use crate::NetworkJsonTransport;
/// Describes a node in the network map tree.
#[derive(Debug, Clone)]
pub struct NetworkJsonNode {
/// The node name, as it appears in `network.json`
pub name: String,
/// The maximum throughput allowed per `network.json` for this node
pub max_throughput: (u32, u32), // In mbps
/// Current throughput (in bytes/second) at this node
pub current_throughput: DownUpOrder<u64>, // In bytes
/// Current TCP Retransmits
pub current_tcp_retransmits: DownUpOrder<u64>, // In retries
/// Current Cake Marks
pub current_marks: DownUpOrder<u64>,
/// Current Cake Drops
pub current_drops: DownUpOrder<u64>,
/// Approximate RTTs reported for this level of the tree.
/// It's never going to be as statistically accurate as the actual
/// numbers, being based on medians.
pub rtts: DashSet<u16>,
/// A list of indices in the `NetworkJson` vector of nodes
/// linking to parent nodes
pub parents: Vec<usize>,
/// The immediate parent node
pub immediate_parent: Option<usize>,
/// The node type
pub node_type: Option<String>,
}
impl NetworkJsonNode {
/// Make a deep copy of a `NetworkJsonNode`, converting atomics
/// into concrete values.
pub fn clone_to_transit(&self) -> NetworkJsonTransport {
NetworkJsonTransport {
name: self.name.clone(),
max_throughput: self.max_throughput,
current_throughput: (
self.current_throughput.get_down(),
self.current_throughput.get_up(),
),
current_retransmits: (
self.current_tcp_retransmits.get_down(),
self.current_tcp_retransmits.get_up(),
),
current_marks: (
self.current_marks.get_down(),
self.current_marks.get_up(),
),
current_drops: (
self.current_drops.get_down(),
self.current_drops.get_up(),
),
rtts: self.rtts.iter().map(|n| *n as f32 / 100.0).collect(),
parents: self.parents.clone(),
immediate_parent: self.immediate_parent,
node_type: self.node_type.clone(),
}
}
}

View File

@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
/// A "transport-friendly" version of `NetworkJsonNode`. Designed
/// to be quickly cloned from original nodes and efficiently
/// transmitted/received.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NetworkJsonTransport {
/// Display name
pub name: String,
/// Max throughput for node (not clamped)
pub max_throughput: (u32, u32),
/// Current node throughput
pub current_throughput: (u64, u64),
/// Current count of TCP retransmits
pub current_retransmits: (u64, u64),
/// Cake marks
pub current_marks: (u64, u64),
/// Cake drops
pub current_drops: (u64, u64),
/// Set of RTT data
pub rtts: Vec<f32>,
/// Node indices of parents
pub parents: Vec<usize>,
/// The immediate parent node in the tree
pub immediate_parent: Option<usize>,
/// The type of node (site, ap, etc.)
#[serde(rename = "type")]
pub node_type: Option<String>,
}

View File

@ -1,5 +1,7 @@
mod serializable;
mod shaped_device;
use std::net::IpAddr;
use crate::SUPPORTED_CUSTOMERS;
use csv::{QuoteStyle, ReaderBuilder, WriterBuilder};
use log::error;
@ -7,6 +9,7 @@ use serializable::SerializableShapedDevice;
pub use shaped_device::ShapedDevice;
use std::path::{Path, PathBuf};
use thiserror::Error;
use lqos_utils::XdpIpAddress;
/// Provides handling of the `ShapedDevices.csv` file that maps
/// circuits to traffic shaping.
@ -166,6 +169,21 @@ impl ConfigShapedDevices {
//println!("Would write to file: {}", csv);
Ok(())
}
/// Helper function to search for an XdpIpAddress and return a circuit id and name
/// if they exist.
pub fn get_circuit_id_and_name_from_ip(&self, ip: &XdpIpAddress) -> Option<(String, String)> {
let lookup = match ip.as_ip() {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
if let Some(c) = self.trie.longest_match(lookup) {
let device = &self.devices[*c.1];
return Some((device.circuit_id.clone(), device.circuit_name.clone()));
}
None
}
}
#[derive(Error, Debug)]

View File

@ -9,8 +9,8 @@ lqos_utils = { path = "../lqos_utils" }
lqos_bus = { path = "../lqos_bus" }
lqos_sys = { path = "../lqos_sys" }
lqos_config = { path = "../lqos_config" }
log = "0"
zerocopy = {version = "0.6.1", features = [ "simd" ] }
once_cell = "1.17.1"
dashmap = "5.4.0"
anyhow = "1"
log = { workspace = true }
zerocopy = { workspace = true }
once_cell = { workspace = true}
dashmap = { workspace = true }
anyhow = { workspace = true }

View File

@ -1,29 +0,0 @@
[package]
name = "lqos_node_manager"
version = "0.1.0"
edition = "2021"
license = "GPL-2.0-only"
[features]
default = ["equinix_tests"]
equinix_tests = []
[dependencies]
rocket = { version = "0.5.1", features = [ "json", "msgpack", "uuid" ] }
rocket_async_compression = "0.6.0"
lqos_bus = { path = "../lqos_bus" }
lqos_config = { path = "../lqos_config" }
lqos_utils = { path = "../lqos_utils" }
anyhow = "1"
sysinfo = "0"
default-net = "0"
nix = "0"
once_cell = "1"
dns-lookup = "1"
dashmap = "5"
reqwest = { version = "0.11.20", features = ["json"] }
lqos_support_tool = { path = "../lqos_support_tool" }
# Support JemAlloc on supported platforms
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
jemallocator = "0.5"

View File

@ -1,3 +0,0 @@
[default]
port = 9123
address = "::"

View File

@ -1,7 +0,0 @@
use std::process::Command;
fn main() {
// Adds a git commit hash to the program
let output = Command::new("git").args(["rev-parse", "HEAD"]).output().unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
}

View File

@ -1,135 +0,0 @@
use std::sync::Mutex;
use anyhow::Error;
use lqos_config::{UserRole, WebUsers};
use once_cell::sync::Lazy;
use rocket::serde::{json::Json, Deserialize, Serialize};
use rocket::{
http::{Cookie, CookieJar, Status},
request::{FromRequest, Outcome},
Request,
};
static WEB_USERS: Lazy<Mutex<Option<WebUsers>>> =
Lazy::new(|| Mutex::new(None));
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthGuard {
Admin,
ReadOnly,
FirstUse,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthGuard {
type Error = anyhow::Error; // Decorated because Error=Error looks odd
async fn from_request(
request: &'r Request<'_>,
) -> Outcome<Self, Self::Error> {
let mut lock = WEB_USERS.lock().unwrap();
if lock.is_none() {
if WebUsers::does_users_file_exist().unwrap() {
*lock = Some(WebUsers::load_or_create().unwrap());
} else {
// There is no user list, so we're redirecting to the
// new user page.
return Outcome::Success(AuthGuard::FirstUse);
}
}
if let Some(users) = &*lock {
if let Some(token) = request.cookies().get("User-Token") {
match users.get_role_from_token(token.value()) {
Ok(UserRole::Admin) => return Outcome::Success(AuthGuard::Admin),
Ok(UserRole::ReadOnly) => {
return Outcome::Success(AuthGuard::ReadOnly)
}
_ => {
return Outcome::Error((
Status::Unauthorized,
Error::msg("Invalid token"),
))
}
}
} else {
// If no login, do we allow anonymous?
if users.do_we_allow_anonymous() {
return Outcome::Success(AuthGuard::ReadOnly);
}
}
}
Outcome::Error((Status::Unauthorized, Error::msg("Access Denied")))
}
}
impl AuthGuard {}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct FirstUser {
pub allow_anonymous: bool,
pub username: String,
pub password: String,
}
#[post("/api/create_first_user", data = "<info>")]
pub fn create_first_user(
cookies: &CookieJar,
info: Json<FirstUser>,
) -> Json<String> {
if WebUsers::does_users_file_exist().unwrap() {
return Json("ERROR".to_string());
}
let mut lock = WEB_USERS.lock().unwrap();
let mut users = WebUsers::load_or_create().unwrap();
users.allow_anonymous(info.allow_anonymous).unwrap();
let token = users
.add_or_update_user(&info.username, &info.password, UserRole::Admin)
.unwrap();
cookies.add(Cookie::new("User-Token", token));
*lock = Some(users);
Json("OK".to_string())
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct LoginAttempt {
pub username: String,
pub password: String,
}
#[post("/api/login", data = "<info>")]
pub fn login(cookies: &CookieJar, info: Json<LoginAttempt>) -> Json<String> {
let mut lock = WEB_USERS.lock().unwrap();
if lock.is_none() && WebUsers::does_users_file_exist().unwrap() {
*lock = Some(WebUsers::load_or_create().unwrap());
}
if let Some(users) = &*lock {
if let Ok(token) = users.login(&info.username, &info.password) {
cookies.add(Cookie::new("User-Token", token));
return Json("OK".to_string());
}
}
Json("ERROR".to_string())
}
#[get("/api/admin_check")]
pub fn admin_check(auth: AuthGuard) -> Json<bool> {
match auth {
AuthGuard::Admin => Json(true),
_ => Json(false),
}
}
#[get("/api/username")]
pub fn username(_auth: AuthGuard, cookies: &CookieJar) -> Json<String> {
if let Some(token) = cookies.get("User-Token") {
let lock = WEB_USERS.lock().unwrap();
if let Some(users) = &*lock {
return Json(users.get_username(token.value()));
}
}
Json("Anonymous".to_string())
}

View File

@ -1,50 +0,0 @@
use rocket::http::Header;
use rocket::response::Responder;
/// Use to wrap a responder when you want to tell the user's
/// browser to try and cache a response.
///
/// For example:
///
/// ```
/// pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
/// LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())
/// }
/// ```
#[derive(Responder)]
pub struct LongCache<T> {
inner: T,
my_header: Header<'static>,
}
impl<'r, 'o: 'r, T: Responder<'r, 'o>> LongCache<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
my_header: Header::new("cache-control", "max-age=604800, public"),
}
}
}
/// Use to wrap a responder when you want to tell the user's
/// browser to keep data private and never cahce it.
///
/// For example:
///
/// ```
/// pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
/// LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())
/// }
/// ```
#[derive(Responder)]
pub struct NoCache<T> {
inner: T,
my_header: Header<'static>,
}
impl<'r, 'o: 'r, T: Responder<'r, 'o>> NoCache<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
my_header: Header::new("cache-control", "no-cache, private"),
}
}
}

View File

@ -1,145 +0,0 @@
use crate::{auth_guard::AuthGuard, cache_control::NoCache};
use default_net::get_interfaces;
use lqos_bus::{bus_request, BusRequest, BusResponse};
use lqos_config::{Tunables, Config, ShapedDevice};
use rocket::{fs::NamedFile, serde::{json::Json, Serialize, Deserialize}};
use rocket::serde::json::Value;
use crate::tracker::SHAPED_DEVICES;
#[get("/api/node_name")]
pub async fn get_node_name() -> Json<String> {
if let Ok(config) = lqos_config::load_config() {
Json(config.node_name)
} else {
Json("No Name Provided".to_string())
}
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/config")]
pub async fn config_page<'a>(_auth: AuthGuard) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/config.html").await.ok())
}
#[get("/api/list_nics")]
pub async fn get_nic_list<'a>(
_auth: AuthGuard,
) -> NoCache<Json<Vec<(String, String, String)>>> {
let result = get_interfaces()
.iter()
.map(|eth| {
let mac = if let Some(mac) = &eth.mac_addr {
mac.to_string()
} else {
String::new()
};
(eth.name.clone(), format!("{:?}", eth.if_type), mac)
})
.collect();
NoCache::new(Json(result))
}
#[get("/api/config")]
pub async fn get_current_lqosd_config(
_auth: AuthGuard,
) -> NoCache<Json<Config>> {
let config = lqos_config::load_config().unwrap();
println!("{config:#?}");
NoCache::new(Json(config))
}
#[post("/api/update_config", data = "<data>")]
pub async fn update_lqosd_config(
data: Json<Config>
) -> String {
let config: Config = (*data).clone();
bus_request(vec![BusRequest::UpdateLqosdConfig(Box::new(config))])
.await
.unwrap();
"Ok".to_string()
}
#[derive(Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct NetworkAndDevices {
shaped_devices: Vec<ShapedDevice>,
network_json: Value,
}
#[post("/api/update_network_and_devices", data = "<data>")]
pub async fn update_network_and_devices(
data: Json<NetworkAndDevices>
) -> String {
let config = lqos_config::load_config().unwrap();
// Save network.json
let serialized_string = rocket::serde::json::to_pretty_string(&data.network_json).unwrap();
let net_json_path = std::path::Path::new(&config.lqos_directory).join("network.json");
let net_json_backup_path = std::path::Path::new(&config.lqos_directory).join("network.json.backup");
if net_json_path.exists() {
// Make a backup
std::fs::copy(&net_json_path, net_json_backup_path).unwrap();
}
std::fs::write(net_json_path, serialized_string).unwrap();
// Save the Shaped Devices
let sd_path = std::path::Path::new(&config.lqos_directory).join("ShapedDevices.csv");
let sd_backup_path = std::path::Path::new(&config.lqos_directory).join("ShapedDevices.csv.backup");
if sd_path.exists() {
std::fs::copy(&sd_path, sd_backup_path).unwrap();
}
let mut lock = SHAPED_DEVICES.write().unwrap();
lock.replace_with_new_data(data.shaped_devices.clone());
println!("{:?}", lock.devices);
lock.write_csv(&format!("{}/ShapedDevices.csv", config.lqos_directory)).unwrap();
"Ok".to_string()
}
#[post("/api/lqos_tuning/<period>", data = "<tuning>")]
pub async fn update_lqos_tuning(
auth: AuthGuard,
period: u64,
tuning: Json<Tunables>,
) -> Json<String> {
if auth != AuthGuard::Admin {
return Json("Error: Not authorized".to_string());
}
// Send the update to the server
bus_request(vec![BusRequest::UpdateLqosDTuning(period, (*tuning).clone())])
.await
.unwrap();
// For now, ignore the reply.
Json("OK".to_string())
}
#[derive(Serialize, Clone, Default)]
#[serde(crate = "rocket::serde")]
pub struct LqosStats {
pub bus_requests_since_start: u64,
pub time_to_poll_hosts_us: u64,
pub high_watermark: (u64, u64),
pub tracked_flows: u64,
pub rtt_events_per_second: u64,
}
#[get("/api/stats")]
pub async fn stats() -> NoCache<Json<LqosStats>> {
for msg in bus_request(vec![BusRequest::GetLqosStats]).await.unwrap() {
if let BusResponse::LqosdStats { bus_requests, time_to_poll_hosts, high_watermark, tracked_flows, rtt_events_per_second } = msg {
return NoCache::new(Json(LqosStats {
bus_requests_since_start: bus_requests,
time_to_poll_hosts_us: time_to_poll_hosts,
high_watermark,
tracked_flows,
rtt_events_per_second,
}));
}
}
NoCache::new(Json(LqosStats::default()))
}

View File

@ -1,93 +0,0 @@
use lqos_bus::{bus_request, BusRequest, BusResponse, FlowbeeSummaryData};
use rocket::serde::json::Json;
use crate::cache_control::NoCache;
#[get("/api/flows/dump_all")]
pub async fn all_flows_debug_dump() -> NoCache<Json<Vec<FlowbeeSummaryData>>> {
let responses =
bus_request(vec![BusRequest::DumpActiveFlows]).await.unwrap();
let result = match &responses[0] {
BusResponse::AllActiveFlows(flowbee) => flowbee.to_owned(),
_ => Vec::new(),
};
NoCache::new(Json(result))
}
#[get("/api/flows/count")]
pub async fn count_flows() -> NoCache<Json<u64>> {
let responses =
bus_request(vec![BusRequest::CountActiveFlows]).await.unwrap();
let result = match &responses[0] {
BusResponse::CountActiveFlows(count) => *count,
_ => 0,
};
NoCache::new(Json(result))
}
#[get("/api/flows/top/<top_n>/<flow_type>")]
pub async fn top_5_flows(top_n: u32, flow_type: String) -> NoCache<Json<Vec<FlowbeeSummaryData>>> {
let flow_type = match flow_type.as_str() {
"rate" => lqos_bus::TopFlowType::RateEstimate,
"bytes" => lqos_bus::TopFlowType::Bytes,
"packets" => lqos_bus::TopFlowType::Packets,
"drops" => lqos_bus::TopFlowType::Drops,
"rtt" => lqos_bus::TopFlowType::RoundTripTime,
_ => lqos_bus::TopFlowType::RateEstimate,
};
let responses =
bus_request(vec![BusRequest::TopFlows { n: top_n, flow_type }]).await.unwrap();
let result = match &responses[0] {
BusResponse::TopFlows(flowbee) => flowbee.to_owned(),
_ => Vec::new(),
};
NoCache::new(Json(result))
}
#[get("/api/flows/by_country")]
pub async fn flows_by_country() -> NoCache<Json<Vec<(String, [u64; 2], [f32; 2])>>> {
let responses =
bus_request(vec![BusRequest::CurrentEndpointsByCountry]).await.unwrap();
let result = match &responses[0] {
BusResponse::CurrentEndpointsByCountry(country_summary) => country_summary.to_owned(),
_ => Vec::new(),
};
NoCache::new(Json(result))
}
#[get("/api/flows/lat_lon")]
pub async fn flows_lat_lon() -> NoCache<Json<Vec<(f64, f64, String, u64, f32)>>> {
let responses =
bus_request(vec![BusRequest::CurrentEndpointLatLon]).await.unwrap();
let result = match &responses[0] {
BusResponse::CurrentLatLon(lat_lon) => lat_lon.to_owned(),
_ => Vec::new(),
};
NoCache::new(Json(result))
}
#[get("/api/flows/ether_protocol")]
pub async fn flows_ether_protocol() -> NoCache<Json<BusResponse>> {
let responses =
bus_request(vec![BusRequest::EtherProtocolSummary]).await.unwrap();
let result = responses[0].to_owned();
NoCache::new(Json(result))
}
#[get("/api/flows/ip_protocol")]
pub async fn flows_ip_protocol() -> NoCache<Json<Vec<(String, (u64, u64))>>> {
let responses =
bus_request(vec![BusRequest::IpProtocolSummary]).await.unwrap();
let result = match &responses[0] {
BusResponse::IpProtocols(ip_protocols) => ip_protocols.to_owned(),
_ => Vec::new(),
};
NoCache::new(Json(result))
}

View File

@ -1,143 +0,0 @@
#[macro_use]
extern crate rocket;
use rocket::fairing::AdHoc;
mod cache_control;
mod shaped_devices;
mod static_pages;
mod tracker;
mod unknown_devices;
use rocket_async_compression::Compression;
mod auth_guard;
mod config_control;
mod network_tree;
mod queue_info;
mod toasts;
mod flow_monitor;
mod support;
// Use JemAllocator only on supported platforms
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
use jemallocator::Jemalloc;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
#[launch]
fn rocket() -> _ {
let server = rocket::build()
.attach(AdHoc::on_liftoff("Poll lqosd", |_| {
Box::pin(async move {
rocket::tokio::spawn(tracker::update_tracking());
})
}))
.attach(AdHoc::on_liftoff("Poll throughput", |_| {
Box::pin(async move {
rocket::tokio::spawn(tracker::update_total_throughput_buffer());
})
}))
.register("/", catchers![static_pages::login])
.mount(
"/",
routes![
static_pages::index,
static_pages::shaped_devices_csv_page,
static_pages::shaped_devices_add_page,
static_pages::unknown_devices_page,
static_pages::circuit_queue,
static_pages::pretty_map_graph,
static_pages::help_page,
config_control::config_page,
network_tree::tree_page,
static_pages::ip_dump,
// Our JS library
static_pages::lqos_js,
static_pages::lqos_css,
static_pages::klingon,
// API calls
tracker::current_throughput,
tracker::throughput_ring_buffer,
tracker::cpu_usage,
tracker::ram_usage,
tracker::top_10_downloaders,
tracker::worst_10_rtt,
tracker::worst_10_tcp,
tracker::rtt_histogram,
tracker::host_counts,
shaped_devices::all_shaped_devices,
shaped_devices::shaped_devices_count,
shaped_devices::shaped_devices_range,
shaped_devices::shaped_devices_search,
shaped_devices::reload_required,
shaped_devices::reload_libreqos,
unknown_devices::all_unknown_devices,
unknown_devices::unknown_devices_count,
unknown_devices::unknown_devices_range,
unknown_devices::unknown_devices_csv,
queue_info::raw_queue_by_circuit,
queue_info::run_btest,
queue_info::circuit_info,
queue_info::current_circuit_throughput,
queue_info::watch_circuit,
queue_info::flow_stats,
queue_info::packet_dump,
queue_info::pcap,
queue_info::request_analysis,
queue_info::dns_query,
config_control::get_nic_list,
//config_control::get_current_python_config,
config_control::get_current_lqosd_config,
//config_control::update_python_config,
config_control::update_network_and_devices,
config_control::update_lqos_tuning,
config_control::update_lqosd_config,
config_control::get_node_name,
auth_guard::create_first_user,
auth_guard::login,
auth_guard::admin_check,
static_pages::login_page,
auth_guard::username,
network_tree::tree_entry,
network_tree::tree_clients,
network_tree::network_tree_summary,
network_tree::node_names,
network_tree::funnel_for_queue,
network_tree::get_network_json,
config_control::stats,
// Supporting files
static_pages::bootsrap_css,
static_pages::plotly_js,
static_pages::jquery_js,
static_pages::msgpack_js,
static_pages::bootsrap_js,
static_pages::tinylogo,
static_pages::favicon,
static_pages::fontawesome_solid,
static_pages::fontawesome_webfont,
static_pages::fontawesome_woff,
// Front page toast checks
toasts::version_check,
toasts::stats_check,
// Flowbee System
flow_monitor::all_flows_debug_dump,
flow_monitor::count_flows,
flow_monitor::top_5_flows,
flow_monitor::flows_by_country,
flow_monitor::flows_lat_lon,
flow_monitor::flows_ether_protocol,
flow_monitor::flows_ip_protocol,
// Suport System
support::run_sanity_check,
support::gather_support_data,
support::submit_support_data,
],
);
// Compression is slow in debug builds,
// so only enable it on release builds.
if cfg!(debug_assertions) {
server
} else {
server.attach(Compression::fairing())
}
}

View File

@ -1,146 +0,0 @@
use std::net::IpAddr;
use lqos_bus::{bus_request, BusRequest, BusResponse};
use lqos_config::NetworkJsonTransport;
use rocket::{
fs::NamedFile,
serde::{json::Json, Serialize, msgpack::MsgPack},
};
use rocket::serde::json::Value;
use crate::{cache_control::NoCache, tracker::SHAPED_DEVICES};
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/tree")]
pub async fn tree_page<'a>() -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/tree.html").await.ok())
}
#[get("/api/network_tree/<parent>")]
pub async fn tree_entry(
parent: usize,
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
let responses =
bus_request(vec![BusRequest::GetNetworkMap { parent }]).await.unwrap();
let result = match &responses[0] {
BusResponse::NetworkMap(nodes) => nodes.to_owned(),
_ => Vec::new(),
};
NoCache::new(MsgPack(result))
}
#[get("/api/network_tree_summary")]
pub async fn network_tree_summary(
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
let responses =
bus_request(vec![BusRequest::TopMapQueues(4)]).await.unwrap();
let result = match &responses[0] {
BusResponse::NetworkMap(nodes) => nodes.to_owned(),
_ => Vec::new(),
};
NoCache::new(MsgPack(result))
}
#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct CircuitThroughput {
pub id: String,
pub name: String,
pub traffic: (u64, u64),
pub limit: (u64, u64),
}
#[get("/api/tree_clients/<parent>")]
pub async fn tree_clients(
parent: String,
) -> NoCache<MsgPack<Vec<CircuitThroughput>>> {
let mut result = Vec::new();
for msg in
bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter()
{
let devices = SHAPED_DEVICES.read().unwrap();
if let BusResponse::HostCounters(hosts) = msg {
for (ip, down, up) in hosts.iter() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => *ip,
};
if let Some(c) = devices.trie.longest_match(lookup) {
if devices.devices[*c.1].parent_node == parent {
result.push(CircuitThroughput {
id: devices.devices[*c.1].circuit_id.clone(),
name: devices.devices[*c.1].circuit_name.clone(),
traffic: (*down, *up),
limit: (
devices.devices[*c.1].download_max_mbps as u64,
devices.devices[*c.1].upload_max_mbps as u64,
),
});
}
}
}
}
}
NoCache::new(MsgPack(result))
}
#[post("/api/node_names", data = "<nodes>")]
pub async fn node_names(
nodes: Json<Vec<usize>>,
) -> NoCache<Json<Vec<(usize, String)>>> {
let mut result = Vec::new();
for msg in bus_request(vec![BusRequest::GetNodeNamesFromIds(nodes.0)])
.await
.unwrap()
.iter()
{
if let BusResponse::NodeNames(map) = msg {
result.extend_from_slice(map);
}
}
NoCache::new(Json(result))
}
#[get("/api/funnel_for_queue/<circuit_id>")]
pub async fn funnel_for_queue(
circuit_id: String,
) -> NoCache<MsgPack<Vec<(usize, NetworkJsonTransport)>>> {
let mut result = Vec::new();
let target = SHAPED_DEVICES
.read()
.unwrap()
.devices
.iter()
.find(|d| d.circuit_id == circuit_id)
.as_ref()
.unwrap()
.parent_node
.clone();
for msg in
bus_request(vec![BusRequest::GetFunnel { target }]).await.unwrap().iter()
{
if let BusResponse::NetworkMap(map) = msg {
result.extend_from_slice(map);
}
}
NoCache::new(MsgPack(result))
}
#[get("/api/network_json")]
pub async fn get_network_json() -> NoCache<Json<Value>> {
if let Ok(config) = lqos_config::load_config() {
let path = std::path::Path::new(&config.lqos_directory).join("network.json");
if path.exists() {
let raw = std::fs::read_to_string(path).unwrap();
let json: Value = rocket::serde::json::from_str(&raw).unwrap();
return NoCache::new(Json(json));
}
}
NoCache::new(Json(Value::String("Not done yet".to_string())))
}

View File

@ -1,202 +0,0 @@
use crate::auth_guard::AuthGuard;
use crate::cache_control::NoCache;
use crate::tracker::{SHAPED_DEVICES, lookup_dns};
use lqos_bus::{bus_request, BusRequest, BusResponse, FlowbeeSummaryData, PacketHeader, QueueStoreTransit};
use rocket::fs::NamedFile;
use rocket::http::Status;
use rocket::response::content::RawJson;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use rocket::serde::msgpack::MsgPack;
use std::net::IpAddr;
#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct CircuitInfo {
pub name: String,
pub capacity: (u64, u64),
}
#[get("/api/watch_circuit/<circuit_id>")]
pub async fn watch_circuit(
circuit_id: String,
_auth: AuthGuard,
) -> NoCache<Json<String>> {
bus_request(vec![BusRequest::WatchQueue(circuit_id)]).await.unwrap();
NoCache::new(Json("OK".to_string()))
}
#[get("/api/circuit_info/<circuit_id>")]
pub async fn circuit_info(
circuit_id: String,
_auth: AuthGuard,
) -> NoCache<MsgPack<CircuitInfo>> {
if let Some(device) = SHAPED_DEVICES
.read()
.unwrap()
.devices
.iter()
.find(|d| d.circuit_id == circuit_id)
{
let result = CircuitInfo {
name: device.circuit_name.clone(),
capacity: (
device.download_max_mbps as u64 * 1_000_000,
device.upload_max_mbps as u64 * 1_000_000,
),
};
NoCache::new(MsgPack(result))
} else {
let result = CircuitInfo {
name: "Nameless".to_string(),
capacity: (1_000_000, 1_000_000),
};
NoCache::new(MsgPack(result))
}
}
#[get("/api/circuit_throughput/<circuit_id>")]
pub async fn current_circuit_throughput(
circuit_id: String,
_auth: AuthGuard,
) -> NoCache<MsgPack<Vec<(String, u64, u64)>>> {
let mut result = Vec::new();
// Get a list of host counts
// This is really inefficient, but I'm struggling to find a better way.
// TODO: Fix me up
for msg in
bus_request(vec![BusRequest::GetHostCounter]).await.unwrap().iter()
{
if let BusResponse::HostCounters(hosts) = msg {
let devices = SHAPED_DEVICES.read().unwrap();
for (ip, down, up) in hosts.iter() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => *ip,
};
if let Some(c) = devices.trie.longest_match(lookup) {
if devices.devices[*c.1].circuit_id == circuit_id {
result.push((ip.to_string(), *down, *up));
}
}
}
}
}
NoCache::new(MsgPack(result))
}
#[get("/api/raw_queue_by_circuit/<circuit_id>")]
pub async fn raw_queue_by_circuit(
circuit_id: String,
_auth: AuthGuard,
) -> NoCache<MsgPack<QueueStoreTransit>> {
let responses =
bus_request(vec![BusRequest::GetRawQueueData(circuit_id)]).await.unwrap();
let result = match &responses[0] {
BusResponse::RawQueueData(Some(msg)) => {
*msg.clone()
}
_ => QueueStoreTransit::default()
};
NoCache::new(MsgPack(result))
}
#[get("/api/flows/<ip_list>")]
pub async fn flow_stats(ip_list: String, _auth: AuthGuard) -> NoCache<Json<Vec<FlowbeeSummaryData>>> {
let mut result = Vec::new();
let request: Vec<BusRequest> = ip_list.split(',').map(|ip| BusRequest::FlowsByIp(ip.to_string())).collect();
let responses = bus_request(request).await.unwrap();
for r in responses.iter() {
if let BusResponse::FlowsByIp(flow) = r {
result.extend_from_slice(flow);
}
}
NoCache::new(Json(result))
}
/*#[get("/api/flows/<ip_list>")]
pub async fn flow_stats(ip_list: String, _auth: AuthGuard) -> NoCache<MsgPack<Vec<(FlowTransport, Option<FlowTransport>)>>> {
let mut result = Vec::new();
let request: Vec<BusRequest> = ip_list.split(',').map(|ip| BusRequest::GetFlowStats(ip.to_string())).collect();
let responses = bus_request(request).await.unwrap();
for r in responses.iter() {
if let BusResponse::FlowData(flow) = r {
result.extend_from_slice(flow);
}
}
NoCache::new(MsgPack(result))
}*/
#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub enum RequestAnalysisResult {
Fail,
Ok{ session_id: usize, countdown: usize }
}
#[get("/api/request_analysis/<ip>")]
pub async fn request_analysis(ip: String) -> NoCache<Json<RequestAnalysisResult>> {
for r in bus_request(vec![BusRequest::GatherPacketData(ip)]).await.unwrap() {
if let BusResponse::PacketCollectionSession{session_id, countdown} = r {
return NoCache::new(Json(RequestAnalysisResult::Ok{session_id, countdown}));
}
}
NoCache::new(Json(RequestAnalysisResult::Fail))
}
#[get("/api/packet_dump/<id>")]
pub async fn packet_dump(id: usize, _auth: AuthGuard) -> NoCache<Json<Vec<PacketHeader>>> {
let mut result = Vec::new();
for r in bus_request(vec![BusRequest::GetPacketHeaderDump(id)]).await.unwrap() {
if let BusResponse::PacketDump(Some(packets)) = r {
result.extend(packets);
}
}
NoCache::new(Json(result))
}
#[allow(unused_variables)]
#[get("/api/pcap/<id>/<filename>")]
pub async fn pcap(id: usize, filename: String) -> Result<NoCache<NamedFile>, Status> {
// The unusued _filename parameter is there to allow the changing of the
// filename on the client side. See Github issue 291.
for r in bus_request(vec![BusRequest::GetPcapDump(id)]).await.unwrap() {
if let BusResponse::PcapDump(Some(filename)) = r {
return Ok(NoCache::new(NamedFile::open(filename).await.unwrap()));
}
}
Err(Status::NotFound)
}
#[get("/api/dns/<ip>")]
pub async fn dns_query(ip: String) -> NoCache<String> {
if let Ok(ip) = ip.parse::<IpAddr>() {
NoCache::new(lookup_dns(ip))
} else {
NoCache::new(ip)
}
}
#[cfg(feature = "equinix_tests")]
#[get("/api/run_btest")]
pub async fn run_btest() -> NoCache<RawJson<String>> {
let responses =
bus_request(vec![BusRequest::RequestLqosEquinixTest]).await.unwrap();
let result = match &responses[0] {
BusResponse::Ack => String::new(),
_ => "Unable to request test".to_string(),
};
NoCache::new(RawJson(result))
}
#[cfg(not(feature = "equinix_tests"))]
pub async fn run_btest() -> NoCache<RawJson<String>> {
NoCache::new(RawJson("No!"))
}

View File

@ -1,76 +0,0 @@
use std::sync::atomic::AtomicBool;
use crate::auth_guard::AuthGuard;
use crate::cache_control::NoCache;
use crate::tracker::SHAPED_DEVICES;
use lqos_bus::{bus_request, BusRequest, BusResponse};
use lqos_config::ShapedDevice;
use rocket::serde::json::Json;
static RELOAD_REQUIRED: AtomicBool = AtomicBool::new(false);
#[get("/api/all_shaped_devices")]
pub fn all_shaped_devices(
_auth: AuthGuard,
) -> NoCache<Json<Vec<ShapedDevice>>> {
NoCache::new(Json(SHAPED_DEVICES.read().unwrap().devices.clone()))
}
#[get("/api/shaped_devices_count")]
pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
NoCache::new(Json(SHAPED_DEVICES.read().unwrap().devices.len()))
}
#[get("/api/shaped_devices_range/<start>/<end>")]
pub fn shaped_devices_range(
start: usize,
end: usize,
_auth: AuthGuard,
) -> NoCache<Json<Vec<ShapedDevice>>> {
let reader = SHAPED_DEVICES.read().unwrap();
let result: Vec<ShapedDevice> =
reader.devices.iter().skip(start).take(end).cloned().collect();
NoCache::new(Json(result))
}
#[get("/api/shaped_devices_search/<term>")]
pub fn shaped_devices_search(
term: String,
_auth: AuthGuard,
) -> NoCache<Json<Vec<ShapedDevice>>> {
let term = term.trim().to_lowercase();
let reader = SHAPED_DEVICES.read().unwrap();
let result: Vec<ShapedDevice> = reader
.devices
.iter()
.filter(|s| {
s.circuit_name.trim().to_lowercase().contains(&term)
|| s.device_name.trim().to_lowercase().contains(&term)
})
.cloned()
.collect();
NoCache::new(Json(result))
}
#[get("/api/reload_required")]
pub fn reload_required() -> NoCache<Json<bool>> {
NoCache::new(Json(
RELOAD_REQUIRED.load(std::sync::atomic::Ordering::Relaxed),
))
}
#[get("/api/reload_libreqos")]
pub async fn reload_libreqos(auth: AuthGuard) -> NoCache<Json<String>> {
if auth != AuthGuard::Admin {
return NoCache::new(Json("Not authorized".to_string()));
}
// Send request to lqosd
let responses = bus_request(vec![BusRequest::ReloadLibreQoS]).await.unwrap();
let result = match &responses[0] {
BusResponse::ReloadLibreQoS(msg) => msg.clone(),
_ => "Unable to reload LibreQoS".to_string(),
};
RELOAD_REQUIRED.store(false, std::sync::atomic::Ordering::Relaxed);
NoCache::new(Json(result))
}

View File

@ -1,168 +0,0 @@
use crate::{
auth_guard::AuthGuard,
cache_control::{LongCache, NoCache},
};
use rocket::fs::NamedFile;
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/")]
pub async fn index<'a>(auth: AuthGuard) -> NoCache<Option<NamedFile>> {
match auth {
AuthGuard::FirstUse => {
NoCache::new(NamedFile::open("static/first_run.html").await.ok())
}
_ => NoCache::new(NamedFile::open("static/main.html").await.ok()),
}
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[catch(401)]
pub async fn login<'a>() -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/login.html").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/login")]
pub async fn login_page<'a>() -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/login.html").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/shaped")]
pub async fn shaped_devices_csv_page<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/shaped.html").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/circuit_queue")]
pub async fn circuit_queue<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/circuit_queue.html").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/ip_dump")]
pub async fn ip_dump<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/ip_dump.html").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/unknown")]
pub async fn unknown_devices_page<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/unknown-ip.html").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/shaped-add")]
pub async fn shaped_devices_add_page<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/shaped-add.html").await.ok())
}
// Temporary for funsies
#[get("/showoff")]
pub async fn pretty_map_graph<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/showoff.html").await.ok())
}
// Help me obi-wan, you're our only hope
#[get("/help")]
pub async fn help_page<'a>(
_auth: AuthGuard,
) -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/help.html").await.ok())
}
#[get("/vendor/bootstrap.min.css")]
pub async fn bootsrap_css<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/bootstrap.min.css").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/lqos.js")]
pub async fn lqos_js<'a>() -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/lqos.js").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/lqos.css")]
pub async fn lqos_css<'a>() -> NoCache<Option<NamedFile>> {
NoCache::new(NamedFile::open("static/lqos.css").await.ok())
}
// Note that NoCache can be replaced with a cache option
// once the design work is complete.
#[get("/vendor/klingon.ttf")]
pub async fn klingon<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/klingon.ttf").await.ok())
}
#[get("/vendor/plotly-2.16.1.min.js")]
pub async fn plotly_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(
NamedFile::open("static/vendor/plotly-2.16.1.min.js").await.ok(),
)
}
#[get("/vendor/jquery.min.js")]
pub async fn jquery_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/jquery.min.js").await.ok())
}
#[get("/vendor/msgpack.min.js")]
pub async fn msgpack_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/msgpack.min.js").await.ok())
}
#[get("/vendor/bootstrap.bundle.min.js")]
pub async fn bootsrap_js<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(
NamedFile::open("static/vendor/bootstrap.bundle.min.js").await.ok(),
)
}
#[get("/vendor/tinylogo.svg")]
pub async fn tinylogo<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/tinylogo.svg").await.ok())
}
#[get("/favicon.png")]
pub async fn favicon<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/favicon.png").await.ok())
}
/// FontAwesome icons
#[get("/vendor/solid.min.css")]
pub async fn fontawesome_solid<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/solid.min.css").await.ok())
}
#[get("/fonts/fontawesome-webfont.ttf")]
pub async fn fontawesome_webfont<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/fa-webfont.ttf").await.ok())
}
#[get("/fonts/fontawesome-webfont.woff2")]
pub async fn fontawesome_woff<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/vendor/fa-webfont.ttf").await.ok())
}

View File

@ -1,25 +0,0 @@
use once_cell::sync::Lazy;
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize};
const MAX_CPUS_COUNTED: usize = 128;
/// Stores overall CPU usage
pub static CPU_USAGE: Lazy<[AtomicU32; MAX_CPUS_COUNTED]> =
Lazy::new(build_empty_cpu_list);
/// Total number of CPUs detected
pub static NUM_CPUS: AtomicUsize = AtomicUsize::new(0);
/// Total RAM used (bytes)
pub static RAM_USED: AtomicU64 = AtomicU64::new(0);
/// Total RAM installed (bytes)
pub static TOTAL_RAM: AtomicU64 = AtomicU64::new(0);
fn build_empty_cpu_list() -> [AtomicU32; MAX_CPUS_COUNTED] {
let mut temp = Vec::with_capacity(MAX_CPUS_COUNTED);
for _ in 0..MAX_CPUS_COUNTED {
temp.push(AtomicU32::new(0));
}
temp.try_into().expect("This should never happen, sizes are constant.")
}

View File

@ -1,38 +0,0 @@
//! Implements a lock-free DNS least-recently-used DNS cache.
use std::net::IpAddr;
use dashmap::DashMap;
use dns_lookup::lookup_addr;
use lqos_utils::unix_time::unix_now;
use once_cell::sync::Lazy;
const CACHE_SIZE: usize = 1000;
struct DnsEntry {
hostname: String,
last_accessed: u64,
}
static DNS_CACHE: Lazy<DashMap<IpAddr, DnsEntry>> = Lazy::new(|| DashMap::with_capacity(CACHE_SIZE));
pub fn lookup_dns(ip: IpAddr) -> String {
// If the cached value exists, just return it
if let Some(mut dns) = DNS_CACHE.get_mut(&ip) {
if let Ok(now) = unix_now() {
dns.last_accessed = now;
}
return dns.hostname.clone();
}
// If it doesn't, we'll be adding it.
if DNS_CACHE.len() >= CACHE_SIZE {
let mut entries : Vec<(IpAddr, u64)> = DNS_CACHE.iter().map(|v| (*v.key(), v.last_accessed)).collect();
entries.sort_by(|a,b| b.1.cmp(&a.1));
DNS_CACHE.remove(&entries[0].0);
}
let hostname = lookup_addr(&ip).unwrap_or(ip.to_string());
DNS_CACHE.insert(ip, DnsEntry { hostname, last_accessed: unix_now().unwrap_or(0) });
String::new()
}

View File

@ -1,13 +0,0 @@
//! The cache module stores cached data, periodically
//! obtained from the `lqosd` server and other parts
//! of the system.
mod cpu_ram;
mod shaped_devices;
mod throughput;
mod dns_cache;
pub use cpu_ram::*;
pub use shaped_devices::*;
pub use throughput::THROUGHPUT_BUFFER;
pub use dns_cache::lookup_dns;

View File

@ -1,9 +0,0 @@
use lqos_config::ConfigShapedDevices;
use once_cell::sync::Lazy;
use std::sync::RwLock;
/// Global storage of the shaped devices csv data.
/// Updated by the file system watcher whenever
/// the underlying file changes.
pub static SHAPED_DEVICES: Lazy<RwLock<ConfigShapedDevices>> =
Lazy::new(|| RwLock::new(ConfigShapedDevices::default()));

View File

@ -1,71 +0,0 @@
use std::sync::Mutex;
use crate::tracker::ThroughputPerSecond;
use lqos_bus::{bus_request, BusRequest, BusResponse};
use once_cell::sync::Lazy;
pub static THROUGHPUT_BUFFER: Lazy<TotalThroughput> =
Lazy::new(|| TotalThroughput::new());
/// Maintains an in-memory ringbuffer of the last 5 minutes of
/// throughput data.
pub struct TotalThroughput {
inner: Mutex<TotalThroughputInner>
}
struct TotalThroughputInner {
data: Vec<ThroughputPerSecond>,
head: usize,
prev_head: usize,
}
impl TotalThroughput {
/// Create a new throughput ringbuffer system
pub fn new() -> Self {
Self {
inner: Mutex::new(TotalThroughputInner {
data: vec![ThroughputPerSecond::default(); 300],
head: 0,
prev_head: 0,
}),
}
}
/// Run once per second to update the ringbuffer with current data
pub async fn tick(&self) {
if let Ok(messages) =
bus_request(vec![BusRequest::GetCurrentThroughput]).await
{
for msg in messages {
if let BusResponse::CurrentThroughput {
bits_per_second,
packets_per_second,
shaped_bits_per_second,
} = msg
{
let mut lock = self.inner.lock().unwrap();
let head = lock.head;
lock.data[head].bits_per_second = bits_per_second;
lock.data[head].packets_per_second = packets_per_second;
lock.data[head].shaped_bits_per_second = shaped_bits_per_second;
lock.prev_head = lock.head;
lock.head += 1;
lock.head %= 300;
}
}
}
}
/// Retrieve just the current throughput data (1 tick)
pub fn current(&self) -> ThroughputPerSecond {
let lock = self.inner.lock().unwrap();
lock.data[lock.prev_head]
}
/// Retrieve the head (0-299) and the full current throughput
/// buffer. Used to populate the dashboard the first time.
pub fn copy(&self) -> (usize, Vec<ThroughputPerSecond>) {
let lock = self.inner.lock().unwrap();
(lock.head, lock.data.clone())
}
}

View File

@ -1,130 +0,0 @@
//! The Cache mod stores data that is periodically updated
//! on the server-side, to avoid re-requesting repeatedly
//! when there are multiple clients.
use super::cache::*;
use anyhow::Result;
use lqos_config::ConfigShapedDevices;
use lqos_utils::file_watcher::FileWatcher;
use nix::sys::{
time::{TimeSpec, TimeValLike},
timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags},
};
use rocket::tokio::{task::spawn_blocking, time::Instant};
use std::{sync::atomic::AtomicBool, time::Duration};
/// Once per second, update CPU and RAM usage and ask
/// `lqosd` for updated system statistics.
/// Called from the main program as a "fairing", meaning
/// it runs as part of start-up - and keeps running.
/// Designed to never return or fail on error.
pub async fn update_tracking() {
use sysinfo::System;
let mut sys = System::new_all();
spawn_blocking(|| {
info!("Watching for ShapedDevices.csv changes");
let _ = watch_for_shaped_devices_changing();
});
let interval_ms = 1000;
info!("Updating throughput ring buffer at {interval_ms} ms cadence.");
std::thread::sleep(std::time::Duration::from_secs(10));
let monitor_busy = AtomicBool::new(false);
if let Ok(timer) =
TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty())
{
if timer
.set(
Expiration::Interval(TimeSpec::milliseconds(interval_ms as i64)),
TimerSetTimeFlags::TFD_TIMER_ABSTIME,
)
.is_ok()
{
loop {
if timer.wait().is_ok() {
if monitor_busy.load(std::sync::atomic::Ordering::Relaxed) {
warn!("Ring buffer tick fired while another queue read is ongoing. Skipping this cycle.");
} else {
monitor_busy.store(true, std::sync::atomic::Ordering::Relaxed);
//info!("Queue tracking timer fired.");
sys.refresh_cpu();
sys.refresh_memory();
sys
.cpus()
.iter()
.enumerate()
.map(|(i, cpu)| (i, cpu.cpu_usage() as u32)) // Always rounds down
.for_each(|(i, cpu)| {
CPU_USAGE[i].store(cpu, std::sync::atomic::Ordering::Relaxed)
});
NUM_CPUS
.store(sys.cpus().len(), std::sync::atomic::Ordering::Relaxed);
RAM_USED
.store(sys.used_memory(), std::sync::atomic::Ordering::Relaxed);
TOTAL_RAM
.store(sys.total_memory(), std::sync::atomic::Ordering::Relaxed);
monitor_busy.store(false, std::sync::atomic::Ordering::Relaxed);
}
} else {
error!(
"Error in timer wait (Linux fdtimer). This should never happen."
);
}
}
} else {
error!("Unable to set the Linux fdtimer timer interval. Queues will not be monitored.");
}
} else {
error!("Unable to acquire Linux fdtimer. Queues will not be monitored.");
}
}
fn load_shaped_devices() {
info!("ShapedDevices.csv has changed. Attempting to load it.");
let shaped_devices = ConfigShapedDevices::load();
if let Ok(new_file) = shaped_devices {
info!("ShapedDevices.csv loaded");
*SHAPED_DEVICES.write().unwrap() = new_file;
} else {
warn!("ShapedDevices.csv failed to load, see previous error messages. Reverting to empty set.");
*SHAPED_DEVICES.write().unwrap() = ConfigShapedDevices::default();
}
}
/// Fires up a Linux file system watcher than notifies
/// when `ShapedDevices.csv` changes, and triggers a reload.
fn watch_for_shaped_devices_changing() -> Result<()> {
let watch_path = ConfigShapedDevices::path();
if watch_path.is_err() {
error!("Unable to generate path for ShapedDevices.csv");
return Err(anyhow::Error::msg(
"Unable to create path for ShapedDevices.csv",
));
}
let watch_path = watch_path.unwrap();
let mut watcher = FileWatcher::new("ShapedDevices.csv", watch_path);
watcher.set_file_exists_callback(load_shaped_devices);
watcher.set_file_created_callback(load_shaped_devices);
watcher.set_file_changed_callback(load_shaped_devices);
loop {
let result = watcher.watch();
info!("ShapedDevices watcher returned: {result:?}");
}
}
/// Fires once per second and updates the global traffic ringbuffer.
pub async fn update_total_throughput_buffer() {
loop {
let now = Instant::now();
THROUGHPUT_BUFFER.tick().await;
let elapsed = now.elapsed();
if elapsed < Duration::from_secs(1) {
rocket::tokio::time::sleep(Duration::from_secs(1) - elapsed).await;
}
}
}

View File

@ -1,196 +0,0 @@
mod cache;
mod cache_manager;
use std::net::IpAddr;
use self::cache::{
CPU_USAGE, NUM_CPUS, RAM_USED, TOTAL_RAM, THROUGHPUT_BUFFER,
};
use crate::{auth_guard::AuthGuard, cache_control::NoCache};
pub use cache::SHAPED_DEVICES;
pub use cache_manager::{update_tracking, update_total_throughput_buffer};
use lqos_bus::{bus_request, BusRequest, BusResponse, IpStats, TcHandle};
use rocket::serde::{Deserialize, Serialize, msgpack::MsgPack};
pub use cache::lookup_dns;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct IpStatsWithPlan {
pub ip_address: String,
pub bits_per_second: (u64, u64),
pub packets_per_second: (u64, u64),
pub median_tcp_rtt: f32,
pub tc_handle: TcHandle,
pub circuit_id: String,
pub plan: (u32, u32),
pub tcp_retransmits: (u64, u64),
}
impl From<&IpStats> for IpStatsWithPlan {
fn from(i: &IpStats) -> Self {
let mut result = Self {
ip_address: i.ip_address.clone(),
bits_per_second: i.bits_per_second,
packets_per_second: i.packets_per_second,
median_tcp_rtt: i.median_tcp_rtt,
tc_handle: i.tc_handle,
circuit_id: i.circuit_id.clone(),
plan: (0, 0),
tcp_retransmits: i.tcp_retransmits,
};
if !result.circuit_id.is_empty() {
if let Some(circuit) = SHAPED_DEVICES
.read()
.unwrap()
.devices
.iter()
.find(|sd| sd.circuit_id == result.circuit_id)
{
let name = if circuit.circuit_name.len() > 20 {
&circuit.circuit_name[0..20]
} else {
&circuit.circuit_name
};
result.ip_address = format!("{} ({})", name, result.ip_address);
result.plan = (circuit.download_max_mbps, circuit.upload_max_mbps);
}
}
result
}
}
/// Stores total system throughput per second.
#[derive(Debug, Clone, Copy, Serialize, Default)]
#[serde(crate = "rocket::serde")]
pub struct ThroughputPerSecond {
pub bits_per_second: (u64, u64),
pub packets_per_second: (u64, u64),
pub shaped_bits_per_second: (u64, u64),
}
#[get("/api/current_throughput")]
pub async fn current_throughput(
_auth: AuthGuard,
) -> NoCache<MsgPack<ThroughputPerSecond>> {
let result = THROUGHPUT_BUFFER.current();
NoCache::new(MsgPack(result))
}
#[get("/api/throughput_ring_buffer")]
pub async fn throughput_ring_buffer(
_auth: AuthGuard,
) -> NoCache<MsgPack<(usize, Vec<ThroughputPerSecond>)>> {
let result = THROUGHPUT_BUFFER.copy();
NoCache::new(MsgPack(result))
}
#[get("/api/cpu")]
pub fn cpu_usage(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u32>>> {
let usage: Vec<u32> = CPU_USAGE
.iter()
.take(NUM_CPUS.load(std::sync::atomic::Ordering::Relaxed))
.map(|cpu| cpu.load(std::sync::atomic::Ordering::Relaxed))
.collect();
NoCache::new(MsgPack(usage))
}
#[get("/api/ram")]
pub fn ram_usage(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u64>>> {
let ram_usage = RAM_USED.load(std::sync::atomic::Ordering::Relaxed);
let total_ram = TOTAL_RAM.load(std::sync::atomic::Ordering::Relaxed);
NoCache::new(MsgPack(vec![ram_usage, total_ram]))
}
#[get("/api/top_10_downloaders")]
pub async fn top_10_downloaders(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
if let Ok(messages) = bus_request(vec![BusRequest::GetTopNDownloaders { start: 0, end: 10 }]).await
{
for msg in messages {
if let BusResponse::TopDownloaders(stats) = msg {
let result = stats.iter().map(|tt| tt.into()).collect();
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
}
#[get("/api/worst_10_rtt")]
pub async fn worst_10_rtt(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
if let Ok(messages) = bus_request(vec![BusRequest::GetWorstRtt { start: 0, end: 10 }]).await
{
for msg in messages {
if let BusResponse::WorstRtt(stats) = msg {
let result = stats.iter().map(|tt| tt.into()).collect();
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
}
#[get("/api/worst_10_tcp")]
pub async fn worst_10_tcp(_auth: AuthGuard) -> NoCache<MsgPack<Vec<IpStatsWithPlan>>> {
if let Ok(messages) = bus_request(vec![BusRequest::GetWorstRetransmits { start: 0, end: 10 }]).await
{
for msg in messages {
if let BusResponse::WorstRetransmits(stats) = msg {
let result = stats.iter().map(|tt| tt.into()).collect();
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
}
#[get("/api/rtt_histogram")]
pub async fn rtt_histogram(_auth: AuthGuard) -> NoCache<MsgPack<Vec<u32>>> {
if let Ok(messages) = bus_request(vec![BusRequest::RttHistogram]).await
{
for msg in messages {
if let BusResponse::RttHistogram(stats) = msg {
let result = stats;
return NoCache::new(MsgPack(result));
}
}
}
NoCache::new(MsgPack(Vec::new()))
}
#[get("/api/host_counts")]
pub async fn host_counts(_auth: AuthGuard) -> NoCache<MsgPack<(u32, u32)>> {
let mut host_counts = (0, 0);
if let Ok(messages) = bus_request(vec![BusRequest::AllUnknownIps]).await {
for msg in messages {
if let BusResponse::AllUnknownIps(unknowns) = msg {
let really_unknown: Vec<IpStats> = unknowns
.iter()
.filter(|ip| {
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
SHAPED_DEVICES.read().unwrap().trie.longest_match(lookup).is_none()
} else {
false
}
})
.cloned()
.collect();
host_counts = (really_unknown.len() as u32, 0);
}
}
}
let n_devices = SHAPED_DEVICES.read().unwrap().devices.len();
let unknown = host_counts.0 - host_counts.1;
NoCache::new(MsgPack((n_devices as u32, unknown)))
}

View File

@ -1,68 +0,0 @@
use std::net::IpAddr;
use crate::{
auth_guard::AuthGuard, cache_control::NoCache, tracker::SHAPED_DEVICES
};
use lqos_bus::{IpStats, bus_request, BusRequest, BusResponse};
use rocket::serde::json::Json;
async fn unknown_devices() -> Vec<IpStats> {
if let Ok(messages) = bus_request(vec![BusRequest::AllUnknownIps]).await {
for msg in messages {
if let BusResponse::AllUnknownIps(unknowns) = msg {
let cfg = SHAPED_DEVICES.read().unwrap();
let really_unknown: Vec<IpStats> = unknowns
.iter()
.filter(|ip| {
if let Ok(ip) = ip.ip_address.parse::<IpAddr>() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
cfg.trie.longest_match(lookup).is_none()
} else {
false
}
})
.cloned()
.collect();
return really_unknown;
}
}
}
Vec::new()
}
#[get("/api/all_unknown_devices")]
pub async fn all_unknown_devices(_auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> {
NoCache::new(Json(unknown_devices().await))
}
#[get("/api/unknown_devices_count")]
pub async fn unknown_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
NoCache::new(Json(unknown_devices().await.len()))
}
#[get("/api/unknown_devices_range/<start>/<end>")]
pub async fn unknown_devices_range(
start: usize,
end: usize,
_auth: AuthGuard,
) -> NoCache<Json<Vec<IpStats>>> {
let reader = unknown_devices().await;
let result: Vec<IpStats> =
reader.iter().skip(start).take(end).cloned().collect();
NoCache::new(Json(result))
}
#[get("/api/unknown_devices_csv")]
pub async fn unknown_devices_csv(_auth: AuthGuard) -> NoCache<String> {
let mut result = "IP Address,Download,Upload\n".to_string();
let reader = unknown_devices().await;
for unknown in reader.iter() {
result += &format!("{},{},{}\n", unknown.ip_address, unknown.bits_per_second.0, unknown.bits_per_second.1);
}
NoCache::new(result)
}

View File

@ -1,997 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script>
<script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped
Devices <span id="shapedCount"
class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i>
Reload LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row top-shunt">
<div class="col-sm-12 bg-light center-txt">
<div class="row">
<div class="col-sm-4">
<span id="circuitName" class="bold redact"></span>
</div>
<div class="col-sm-6">
<ul class="nav nav-pills mb-3" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-home-tab" data-bs-toggle="pill"
data-bs-target="#pills-home" type="button" role="tab" aria-controls="pills-home"
aria-selected="true">Overview</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-tins-tab" data-bs-toggle="pill"
data-bs-target="#pills-tins" type="button" role="tab" aria-controls="pills-profile"
aria-selected="false">All Tins</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-funnel-tab" data-bs-toggle="pill"
data-bs-target="#pills-funnel" type="button" role="tab" aria-controls="pills-funnel"
aria-selected="false">Queue Tree</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-flows-tab" data-bs-toggle="pill"
data-bs-target="#pills-flows" type="button" role="tab" aria-controls="pills-flows"
aria-selected="false">Flows</button>
</li>
</ul>
</div>
<div class="col-sm-2">
<a href="#" class="btn btn-small btn-info" id="btnPause"><i class="fa fa-pause"></i> Pause</a>
<a href="#" class="btn btn-small btn-info" id="btnSlow"><i class="fa fa-hourglass"></i> Slow
Mode</a>
</div>
</div>
</div>
</div>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab"
tabindex="0">
<!-- Total Throughput and Backlog -->
<div class="row">
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-dashboard"></i> Throughput</h5>
<div id="throughputGraph" class="graph150"></div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-car"></i> Backlog</h5>
<div id="backlogGraph" class="graph150"></div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> Capacity Quantile (Last 10s)</h5>
<div id="capacityQuantile" class="graph150"></div>
</div>
</div>
</div>
</div>
<!-- Delay and Queue Length -->
<div class="row mtop4">
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-hourglass"></i> Delays</h5>
<div id="delayGraph" class="graph150"></div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-fast-forward"></i> Queue Length</h5>
<div id="qlenGraph" class="graph150"></div>
</div>
</div>
</div>
</div>
<div class="row mtop4">
<div class="col-sm-2">
<div class="card bg-light">
<div class="card-body">
Queue Memory: <span id="memory"></span>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-tins" role="tabpanel" aria-labelledby="pills-tins-tab" tabindex="1">
<div class="row" class="mtop4">
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-truck"></i> Tin 1 (Bulk)</h5>
<div id="tinTp_0" class="graph150"></div>
<div id="tinMd_0" class="graph150"></div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-balance-scale"></i> Tin 2 (Best Effort)</h5>
<div id="tinTp_1" class="graph150"></div>
<div id="tinMd_1" class="graph150"></div>
</div>
</div>
</div>
</div>
<div class="row mtop4">
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-television"></i> Tin 3 (Video)</h5>
<div id="tinTp_2" class="graph150"></div>
<div id="tinMd_2" class="graph150"></div>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-phone"></i> Tin 4 (Voice)</h5>
<div id="tinTp_3" class="graph150"></div>
<div id="tinMd_3" class="graph150"></div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-funnel" role="tabpanel" aria-labelledby="pills-funnel-tab"
tabindex="2">
</div>
<div class="tab-pane fade" id="pills-flows" role="tabpanel" aria-labelledby="pills-flows-tab" tabindex="3">
<div class="row">
<div class="col-sm12">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> Flows (Last 30 Seconds)</h5>
<div id="packetButtons"></div>
<div id="flowList"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
let throughput = new Object();
let throughput_head = 0;
let circuit_info = null;
function nameCircuit() {
if (circuit_info == null) {
msgPackGet("/api/circuit_info/" + encodeURI(id), (data) => {
circuit_info = data;
let capacity = scaleNumber(circuit_info[CircuitInfo.capacity][0]) + " / " + scaleNumber(circuit_info[CircuitInfo.capacity][1]);
$("#circuitName").text(redactText(circuit_info[CircuitInfo.name]) + " " + capacity);
});
}
}
function displayMemory(data) {
// Fill Base Information
let total_memory = data[QD.current_download][CT.memory_used] + data[QD.current_upload][CT.memory_used];
$("#memory").text(scaleNumber(total_memory));
}
class CombinedPlot {
constructor(capacity) {
this.y = []
for (let i = 0; i < capacity * 2; ++i) {
this.y.push(0);
}
}
store(x, y, value) {
if (value == 0) value = null;
this.y[(x * 2) + y] = value;
}
}
class TinsPlot {
constructor(capacity) {
this.tins = [
new CombinedPlot(capacity),
new CombinedPlot(capacity),
new CombinedPlot(capacity),
new CombinedPlot(capacity)
];
}
store(tin, x, y, value) {
this.tins[tin].store(x, y, value);
}
}
class QueuePlotter {
constructor(capacity) {
this.capacity = capacity;
this.x_axis = [];
this.backlog = new TinsPlot(capacity);
this.delays = new TinsPlot(capacity);
this.queueLen = new CombinedPlot(capacity);
this.throughput = new TinsPlot(capacity);
this.drops = new TinsPlot(capacity);
this.marks = new TinsPlot(capacity);
for (let i = 0; i < capacity; ++i) {
this.x_axis.push(i);
this.x_axis.push(i);
}
}
ingestBacklog(subData, currentX, tin) {
this.backlog.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.backlog_bytes] * 8);
this.backlog.store(tin, currentX, 1, 0.0 - subData[1][CDT.tins][tin][CDTT.backlog_bytes] * 8);
}
ingestDelays(subData, currentX, tin) {
let down = subData[0][CDT.tins][tin][CDTT.avg_delay_us] * 0.001;
let up = subData[1][CDT.tins][tin][CDTT.avg_delay_us] * 0.001;
if (down == 0.0) {
down = null;
} else {
down = Math.log10(down);
}
if (up == 0.0) {
up = null;
} else {
//console.log(up);
up = 0.0 - Math.log10(up);
}
this.delays.store(tin, currentX, 0, down);
this.delays.store(tin, currentX, 1, up);
}
ingestQueueLen(subData, currentX) {
this.queueLen.store(currentX, 0, subData[0][CDT.qlen]);
this.queueLen.store(currentX, 1, 0.0 - subData[1][CDT.qlen]);
}
ingestThroughput(subData, currentX, tin) {
this.throughput.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.sent_bytes] * 8);
this.throughput.store(tin, currentX, 1, 0.0 - (subData[1][CDT.tins][tin][CDTT.sent_bytes] * 8));
}
ingestDrops(subData, currentX, tin) {
this.drops.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.drops]);
this.drops.store(tin, currentX, 1, 0.0 - subData[1][CDT.tins][tin][CDTT.drops]);
}
ingestMarks(subData, currentX, tin) {
this.marks.store(tin, currentX, 0, subData[0][CDT.tins][tin][CDTT.marks]);
this.marks.store(tin, currentX, 1, 0.0 - subData[1][CDT.tins][tin][CDTT.marks]);
}
ingest(data, currentX, hi) {
// We're inserting at currentX, from the history entry indexed
// by hi
if (activeTab == "pills-home-tab") {
this.ingestQueueLen(data[QD.history][hi], currentX);
}
for (let tin = 0; tin < 4; ++tin) {
if (data[QD.history][hi][0][3].length == 4 && data[QD.history][hi][1][3].length == 4) {
if (activeTab == "pills-home-tab") {
this.ingestBacklog(data[QD.history][hi], currentX, tin);
this.ingestDelays(data[QD.history][hi], currentX, tin);
} else if (activeTab == "pills-tins-tab") {
this.ingestThroughput(data[QD.history][hi], currentX, tin);
this.ingestDrops(data[QD.history][hi], currentX, tin);
this.ingestMarks(data[QD.history][hi], currentX, tin);
}
}
}
}
update(data) {
// Iterate the whole history ringbuffer
// Note that we're going backwards. reverse() turned out
// to be surprisingly expensive in JS.
let currentX = this.capacity;
for (let hi = data[QD.history_head]; hi < 600; ++hi) {
this.ingest(data, currentX, hi);
currentX--;
}
for (let hi = 0; hi < data[QD.history_head]; ++hi) {
this.ingest(data, currentX, hi);
currentX--;
}
}
plotBacklog() {
let graph = document.getElementById("backlogGraph");
let graphData = [
{ x: this.x_axis, y: this.backlog.tins[0].y, type: 'scattergl', mode: 'markers', name: 'Bulk', marker: { size: 4 } },
{ x: this.x_axis, y: this.backlog.tins[1].y, type: 'scattergl', mode: 'markers', name: 'Best Effort', marker: { size: 4 } },
{ x: this.x_axis, y: this.backlog.tins[2].y, type: 'scattergl', mode: 'markers', name: 'Video', marker: { size: 4 } },
{ x: this.x_axis, y: this.backlog.tins[3].y, type: 'scattergl', mode: 'markers', name: 'Voice', marker: { size: 4 } },
];
if (this.backlogPlotted == null) {
this.backlogPlotted = true;
Plotly.newPlot(
graph,
graphData,
{
margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 },
yaxis: { automargin: true, title: "Bytes" },
xaxis: { automargin: true, title: "Time since now" }
});
} else {
Plotly.redraw(graph, graphData);
}
}
plotDelays() {
let graph = document.getElementById("delayGraph");
let graphData = [
{ x: this.x_axis, y: this.delays.tins[0].y, type: 'scattergl', mode: 'markers', name: 'Bulk', marker: { size: 4 } },
{ x: this.x_axis, y: this.delays.tins[1].y, type: 'scattergl', mode: 'markers', name: 'Best Effort', marker: { size: 4 } },
{ x: this.x_axis, y: this.delays.tins[2].y, type: 'scattergl', mode: 'markers', name: 'Video', marker: { size: 4 } },
{ x: this.x_axis, y: this.delays.tins[3].y, type: 'scattergl', mode: 'markers', name: 'Voice', marker: { size: 4 } },
];
if (this.delaysPlotted == null) {
Plotly.newPlot(
graph,
graphData,
{
margin: { l: 8, r: 0, b: 0, t: 0, pad: 4 },
yaxis: { automargin: true, title: "log10(ms)", range: [-1.0, 1.0] },
xaxis: { automargin: true, title: "Time since now" }
});
this.delaysPlotted = true;
} else {
Plotly.redraw(graph, graphData);
}
}
plotQueueLen() {
let graph = document.getElementById("qlenGraph");
let graphData = [
{ x: this.x_axis, y: this.queueLen.y, type: 'scattergl', mode: 'markers', name: 'Queue Length' },
];
if (this.queueLenPlotted == null) {
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Packets" }, xaxis: { automargin: true, title: "Time since now" } });
this.queueLenPlotted = true;
} else {
Plotly.redraw(graph, graphData);
}
}
plotTinThroughput(tin) {
let graph = document.getElementById("tinTp_" + tin);
let graphData = [
{ x: this.x_axis, y: this.throughput.tins[tin].y, type: 'scatter', mode: 'markers' }
];
if (this.tinsPlotted == null) {
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Bits" }, xaxis: { automargin: true, title: "Time since now" } });
} else {
Plotly.redraw(graph, graphData);
}
}
plotMarksDrops(tin) {
let graph = document.getElementById("tinMd_" + tin);
let graphData = [
{ x: this.x_axis, y: this.drops.tins[tin].y, name: 'Drops', type: 'scatter', mode: 'markers', marker: { size: 4 } },
{ x: this.x_axis, y: this.marks.tins[tin].y, name: 'Marks', type: 'scatter', mode: 'markers', marker: { size: 4 } },
];
if (this.tinsPlotted == null) {
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Packets" }, xaxis: { automargin: true, title: "Time since now" } });
} else {
Plotly.redraw(graph, graphData);
}
}
plot() {
if (activeTab == "pills-home-tab") {
this.plotBacklog();
this.plotDelays();
this.plotQueueLen();
} else if (activeTab == "pills-tins-tab") {
for (let tin = 0; tin < 4; ++tin) {
this.plotTinThroughput(tin);
this.plotMarksDrops(tin);
}
this.tinsPlotted = true;
}
}
}
let qp = null;
function pollQueue() {
if (id != null) {
// Name the circuit
nameCircuit();
// Graphs
msgPackGet("/api/raw_queue_by_circuit/" + encodeURI(id), (data) => {
if (qp == null) qp = new QueuePlotter(600);
qp.update(data);
qp.plot();
displayMemory(data);
});
}
}
let ips = [];
class ThroughputMonitor {
constructor(capacity) {
this.capacity = capacity;
this.head = 0;
this.per_ip = {};
this.y = {};
this.x_axis = [];
for (let i = 0; i < capacity; ++i) {
this.x_axis.push(i);
this.x_axis.push(i);
}
this.quantiles = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
];
}
clearQuantiles() {
for (let i = 0; i < 12; ++i) {
this.quantiles[0][i] = 0;
this.quantiles[1][i] = 0;
}
}
ingest(ip, down, up) {
down = down * 8;
up = up * 8;
if (!this.per_ip.hasOwnProperty(ip)) {
this.per_ip[ip] = [];
this.y[ip] = [];
for (let i = 0; i < this.capacity; ++i) {
this.per_ip[ip].push(0);
this.per_ip[ip].push(0);
this.y[ip].push(0);
this.y[ip].push(0);
}
}
this.per_ip[ip][this.head] = down;
this.per_ip[ip][this.head + 1] = 0.0 - up;
this.head += 2;
if (this.head > this.capacity * 2) {
this.head = 0;
}
}
addQuantile(down, up) {
up = 0 - up;
let down_slot = Math.floor((down / circuit_info[CircuitInfo.capacity][0]) * 10.0);
let up_slot = Math.floor((up / circuit_info[CircuitInfo.capacity][1]) * 10.0);
if (down_slot < 0) down_slot = 0;
if (up_slot < 0) up_slot = 0;
if (down_slot > 10) down_slot = 10;
if (up_slot > 10) up_slot = 10;
this.quantiles[0][down_slot] += 1;
this.quantiles[1][up_slot] += 1;
//console.log(down_slot, up_slot);
}
prepare() {
this.clearQuantiles();
for (const ip in this.per_ip) {
let counter = this.capacity * 2;
for (let i = this.head; i < this.capacity * 2; i++) {
this.y[ip][counter] = this.per_ip[ip][i];
counter--;
}
for (let i = 0; i < this.head; i++) {
this.y[ip][counter] = this.per_ip[ip][i];
counter--;
}
for (let i = 2; i < 22; i += 2) {
this.addQuantile(this.y[ip][i], this.y[ip][i + 1]);
}
}
}
plot(target) {
let graph = document.getElementById(target);
let graphData = [];
for (const ip in this.per_ip) {
graphData.push({ x: this.x_axis, y: this.y[ip], name: ip, mode: 'markers', type: 'scattergl', marker: { size: 3 } });
}
if (!this.hasOwnProperty("plotted" + target)) {
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Traffic (bits)" }, xaxis: { automargin: true, title: "Time since now" } });
this["plotted" + target] = true;
} else {
Plotly.redraw(graph, graphData);
}
}
plotQuantiles() {
let graph = document.getElementById("capacityQuantile");
let graphData = [
{ x: this.quantiles[2], y: this.quantiles[0], name: 'Download', type: 'bar' },
{ x: this.quantiles[2], y: this.quantiles[1], name: 'Upload', type: 'bar' },
];
if (this.plottedQ == null) {
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: '# Samples' }, xaxis: { automargin: true, title: '% Utilization' } });
this.plottedQ = true;
} else {
Plotly.redraw(graph, graphData);
}
}
}
let tpData = null;
function getThroughput() {
if (id != null) {
msgPackGet("/api/circuit_throughput/" + encodeURI(id), (data) => {
if (tpData == null) tpData = new ThroughputMonitor(300);
ips = [];
for (let i = 0; i < data.length; i++) {
let ip = data[i][0];
ips.push(ip);
let down = data[i][1];
let up = data[i][2];
tpData.ingest(ip, down, up);
}
tpData.prepare();
tpData.plot("throughputGraph");
tpData.plotQuantiles();
});
}
}
let funnels = new ThroughputMonitor(300);
let rtts = {};
let circuitId = "";
let builtFunnelDivs = false;
function getFunnel() {
if (builtFunnelDivs) {
plotFunnels();
return;
}
circuitId = encodeURI(id);
msgPackGet("/api/funnel_for_queue/" + circuitId, (data) => {
let html = "";
// Add the client on top
let row = "<div class='row row220'>";
row += "<div class='col-sm-12'>";
row += "<div class='card bg-light'>";
row += "<h5 class='card-title'><i class='fa fa-hourglass'></i> Client Throughput</h5>";
row += "<div id='tp_client' class='graph98 graph150'></div>";
row += "</div>";
row += "</div>";
row += "</div>";
html += row;
// Funnels
for (let i = 0; i < data.length; ++i) {
//funnels.push(data[i][0], data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
funnels.ingest(data[i][0], data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
rtts[data[i][0]] = new RttHistogram();
let row = "<div class='row row220'>";
row += "<div class='col-sm-6'>";
row += "<div class='card bg-light'>";
row += "<h5 class='card-title'><i class='fa fa-hourglass'></i> <a class='redact' href='/tree?parent=" + data[i][0] + "'>" + redactText(data[i][1][NetTrans.name]) + " Throughput</a></h5>";
row += "<div id='tp" + data[i][0] + "' class='graph98 graph150'></div>";
row += "</div>";
row += "</div>";
row += "<div class='col-sm-6'>";
row += "<div class='card bg-light'>";
row += "<h5 class='card-title redact'><i class='fa fa-bar-chart'></i> " + redactText(data[i][1][NetTrans.name]) + " TCP RTT</h5>";
row += "<div id='rtt" + data[i][0] + "' class='graph98 graph150'></div>";
row += "</div>";
row += "</div>";
row += "</div>";
html += row;
}
$("#pills-funnel").html(html);
builtFunnelDivs = true;
});
}
let plottedFunnels = {};
function plotFunnels() {
if (tpData != null) tpData.plot("tp_client");
funnels.prepare();
msgPackGet("/api/funnel_for_queue/" + encodeURI(circuitId), (data) => {
for (let i = 0; i < data.length; ++i) {
rtts[data[i][0]].clear();
funnels.ingest(data[i][0], data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
for (let j = 0; j < data[i][1][NetTrans.rtts].length; j++) {
rtts[data[i][0]].push(data[i][1][NetTrans.rtts][j]);
}
rtts[data[i][0]].plot("rtt" + data[i][0]);
}
for (const [k, v] of Object.entries(funnels.y)) {
let target_div = "tp" + k;
let graphData = [
{ x: funnels.x_axis, y: v, type: 'scatter', mode: 'markers', marker: { size: 3 } }
];
let graph = document.getElementById(target_div);
if (!plotFunnels.hasOwnProperty(target_div)) {
Plotly.newPlot(graph, graphData, { margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 }, yaxis: { automargin: true, title: "Traffic (bits)" }, xaxis: { automargin: true, title: "Time since now" } });
} else {
Plotly.redraw(graph, graphData);
}
}
});
}
function icmpType(n) {
switch (n) {
case 0: return "ECHO REPLY";
case 3: return "DESTINATION UNREACHABLE";
case 4: return "SOURCE QUENCH";
case 8: return "ECHO REQUEST";
case 11: return "TIME EXCEEDED";
case 12: return "PARAMETER PROBLEM";
case 13: return "TIMESTAMP REQUEST";
case 14: return "TIMESTAMP REPLY";
case 15: return "INFO REQUEST";
case 16: return "INFO REPLY";
case 17: return "ADDRESS REQUEST";
case 18: return "ADDRESS REPLY";
default: return "?";
}
}
var madeButtons = false;
var analysisId = null;
var analysisTimer = null;
var analysisBtn = null;
function analyze(id) {
if (analysisId != null) {
alert("Heimdall says: 'STOP CLICKING ME'");
return;
}
let ip = ips[id];
$.get("/api/request_analysis/" + encodeURI(ip), (data) => {
if (data == "Fail") {
alert("Heimdall is busy serving other customers. Your desire is important to him, please try again later.")
return;
}
analysisId = data.Ok.session_id;
analysisBtn = "#dumpBtn_" + id;
analysisTimer = data.Ok.countdown;
analyzeTick();
});
}
function analyzeTick() {
$(analysisBtn).text("Gathering Data for " + analysisTimer + " more seconds");
analysisTimer--;
if (analysisTimer > -1) {
setTimeout(analyzeTick, 1000);
} else {
window.location.href = "/ip_dump?id=" + analysisId + "&circuit_id=" + encodeURI(id);
}
}
function parse_rtts(data, idx) {
let n = [];
for (let i=0; i<data.rtt_ringbuffer[idx].length; i++) {
n.push(data.rtt_ringbuffer[idx][i]);
}
if (n.length == 0) {
return 0.0;
}
n.sort();
// Median
return n[Math.floor(n.length / 2)];
}
function getFlows() {
let ip_list = "";
let ip_btns = "";
for (let i = 0; i < ips.length; ++i) {
ip_list += ips[i] + ",";
if (circuit_info != null) {
ip_btns += "<a id='dumpBtn_" + i + "' href='#' onclick='analyze(\"" + i + "\")' class='btn btn-info'><i class='fa fa-search'></i> Analyze: " + ips[i] + "</a> "
}
}
if (!madeButtons && ips.length > 0 && circuit_info != null) {
ip_btns += "<br />";
madeButtons = true;
$("#packetButtons").html(ip_btns);
}
ip_list = ip_list.substring(0, ip_list.length - 1);
if (ip_list == "") return;
$.get("/api/flows/" + ip_list, (data) => {
//msgPackGet("/api/flows/" + ip_list, (data) => {
//console.log(data);
let html = "<table class='table table-striped'>";
html += "<thead>";
html += "<th>Connection</th>";
html += "<th>Bytes</th>";
html += "<th>Packets</th>";
html += "<th>TCP Retransmits</th>";
html += "<th>TCP RTT</th>";
html += "<th>ASN</th>";
html += "<th>ASN Country</th>";
html += "</thead>";
html += "<tbody>";
for (var i=0; i<data.length; i++) {
console.log(data[i]);
html += "<tr>";
html += "<td>" + data[i].analysis + "</td>";
html += "<td>" + scaleNumber(data[i].bytes_sent[0]) + " / " + scaleNumber(data[i].bytes_sent[1]) + "</td>";
html += "<td>" + scaleNumber(data[i].packets_sent[0]) + " / " + scaleNumber(data[i].packets_sent[1]) + "</td>";
html += "<td>" + data[i].tcp_retransmits[0] + " / " + data[i].tcp_retransmits[1] + "</td>";
html += "<td>" + scaleNanos(data[i].rtt_nanos[0]) + " / " + scaleNanos(data[i].rtt_nanos[1]) + "</td>";
html += "<td>(" + data[i].remote_asn + ") " + data[i].remote_asn_name + "</td>";
html += "<td>" + data[i].remote_asn_country + "</td>";
html += "</tr>";
}
html += "</tbody>";
/*html += "<thead>";
html += "<th>Protocol</th>";
html += "<th>Src</th>";
html += "<th>Src Port</th>";
html += "<th>Dst</th>";
html += "<th>Dst Port</th>";
html += "<th>Pkt In</th>";
html += "<th>Pkt Out</th>";
html += "<th>Bytes In</th>";
html += "<th>Bytes Out</th>";
html += "<th>DSCP In</th>";
html += "<th>DSCP Out</th>";
html += "<th>ECN In</th>";
html += "<th>ECN Out</th>";
html += "</thead>";
for (let i = 0; i < data.length; i++) {
let rpackets = "-";
let rbytes = "-";
let rdscp = "-";
let rcongestion = "-";
if (data[i][1] != null) {
rpackets = data[i][1][FlowTrans.packets];
rbytes = scaleNumber(data[i][1][FlowTrans.bytes]);
rdscp = "0x" + data[i][1][FlowTrans.dscp].toString(16);
rcongestion = ecn(data[i][1][FlowTrans.ecn]);
}
html += "<tr>";
html += "<td>" + data[i][0][FlowTrans.proto] + "</td>";
html += "<td>" + ipToHostname(data[i][0][FlowTrans.src]) + "</td>";
if (data[i][0].proto == "ICMP") {
html += "<td>" + icmpType(data[i][0][FlowTrans.src_port]) + "</td>";
} else {
html += "<td>" + data[i][0][FlowTrans.src_port] + "</td>";
}
html += "<td>" + ipToHostname(data[i][0][FlowTrans.dst]) + "</td>";
if (data[i][0][FlowTrans.proto] == "ICMP") {
if (data[i][1] != null) {
html += "<td>" + icmpType(data[i][1][FlowTrans.src_port]) + "</td>";
} else {
html += "<td></td>";
}
} else {
html += "<td>" + data[i][0][FlowTrans.dst_port] + "</td>";
}
html += "<td>" + data[i][0][FlowTrans.packets] + "</td>";
html += "<td>" + rpackets + "</td>";
html += "<td>" + scaleNumber(data[i][0][FlowTrans.bytes]) + "</td>";
html += "<td>" + rbytes + "</td>";
html += "<td>0x" + data[i][0][FlowTrans.dscp].toString(16) + "</td>";
html += "<td>" + rdscp + "</td>";
html += "<td>" + ecn(data[i][0][FlowTrans.ecn]) + "</td>";
html += "<td>" + rcongestion + "</td>";
html += "</tr>";
}
html += "</tbody></table>";
*/
$("#flowList").html(html);
})
}
let id = 0;
let activeTab = "pills-home-tab";
var lastCalledTime;
var fps;
var worstDelta = 0;
var paused = false;
var slowMode = false;
function showFps() {
if (!lastCalledTime) {
lastCalledTime = Date.now();
fps = 0;
return;
}
delta = (Date.now() - lastCalledTime) / 1000;
lastCalledTime = Date.now();
fps = 1 / delta;
//$("#fps").text(fps.toFixed(0));
worstDelta = Math.max(delta, worstDelta);
}
function updateFrame() {
showFps();
if (!paused) {
switch (activeTab) {
case "pills-funnel-tab": {
getFunnel();
getThroughput();
} break;
case "pills-flows-tab": {
getFlows();
} break;
default: {
pollQueue();
getThroughput();
}
}
}
// Doing this to balance out the FPS
// It will tend towards the slowest
if (slowMode) {
setTimeout(updateFrame, 1000);
} else {
setTimeout(() => {
requestAnimationFrame(updateFrame);
}, worstDelta * 200);
}
}
function wireUpTabEvents() {
// Fire events when the active tab changes
$(document).on('shown.bs.tab', 'button[data-bs-toggle="pill"]', function (e) {
activeTab = e.target.id;
//console.log(activeTab);
});
}
function isSlowMode() {
let slow = localStorage.getItem("slowMode");
if (slow == null) {
localStorage.setItem("slowMode", false);
slow = false;
}
if (slow == "false") {
slow = false;
} else if (slow == "true") {
slow = true;
}
return slow;
}
function start() {
setTitle();
wireUpTabEvents();
$("#btnPause").on('click', () => {
paused = !paused;
if (paused) {
$("#btnPause").html("<i class='fa fa-play'></i> Resume");
} else {
$("#btnPause").html("<i class='fa fa-pause'></i> Pause");
}
});
slowMode = isSlowMode();
if (slowMode) {
$("#btnSlow").html("<i class='fa fa-fast-forward'></i> Fast Mode");
} else {
$("#btnSlow").html("<i class='fa fa-hourglass'></i> Slow Mode");
}
$("#btnSlow").on('click', () => {
slowMode = !slowMode;
localStorage.setItem("slowMode", slowMode);
if (slowMode) {
$("#btnSlow").html("<i class='fa fa-fast-forward'></i> Fast Mode");
} else {
$("#btnSlow").html("<i class='fa fa-hourglass'></i> Slow Mode");
}
});
colorReloadButton();
updateHostCounts();
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
id = params.id;
$.get("/api/watch_circuit/" + params.id, () => {
//updateFrame();
requestAnimationFrame(updateFrame);
});
}
$(document).ready(start);
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<div id="container" class="pad4">
<div class="row">
<div class="col-sm-4"></div>
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title">First Login</h5>
<p>
No <em>lqusers.toml</em> file was found. This is probably the first time you've run
the LibreQoS web system. If it isn't, then please check permissions on that file and
use the "bin/lqusers" command to verify that your system is working.
</p>
<p class="alert alert-warning" role="alert">
This site will use a cookie to store your identification. If that's not ok,
please don't use the site.
</p>
<p>Let's create a new user, and set some parameters:</p>
<table class="table">
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="allowAnonymous">
<label class="form-check-label" for="allowAnonymous">
Allow anonymous users to view (but not change) settings.
</label>
</td>
</tr><tr>
<td>
Your Username
</td>
<td>
<input type="text" id="username" />
</td>
</tr>
<tr>
<td>Your password</td>
<td><input type="password" id="password" /></td>
</tr>
</table>
<a class="btn btn-primary" id="btnCreateUser">Create User Account</a>
</div>
</div>
</div>
<div class="col-sm-4"></div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
function start() {
$("#btnCreateUser").on('click', (data) => {
let newUser = {
allow_anonymous: $("#allowAnonymous").prop('checked'),
username: $("#username").val(),
password: $("#password").val(),
};
$.ajax({
type: "POST",
url: "/api/create_first_user",
data: JSON.stringify(newUser),
success: (data) => {
if (data == "ERROR") {
alert("Unable to create a first user.")
} else {
window.location.href = "/";
}
}
})
});
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,322 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row">
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-question-circle"></i> Help & Support</h5>
<p class="alert-warning">
<i class="fa fa-info-circle"></i>
Priority support is given to Long-Term Stats subscribers and donors. Other support is
best effort, with volunteers trying to help as best we can.
</p>
<p>
<a href="https://github.com/sponsors/LibreQoE/" class="btn btn-primary">
<i class="fa fa-money"></i> Support LibreQoS Today
</a>
</p>
</div>
</div>
</div>
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-book"></i> Documentation</h5>
<p>The documentation is a great place to start!</p>
<a href="https://libreqos.readthedocs.io/en/latest/" class="btn btn-primary"><i class="fa fa-book"></i> Read The LibreQoS Documentation</a>
</div>
</div>
</div>
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-user-circle"></i> Chat</h5>
<p>
Connect with other LibreQoS users, and the LibreQoS team on our Zulip Chat System. <br />
<a class="btn btn-primary" href="https://chat.libreqos.io/"><i class="fa fa-user-circle"></i> Zulip chat system</a>.
</p>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 10px;">
<div class="col-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-wrench"></i> Tools</h5>
<p style="border: 1px solid #eee">
<strong>Sanity check</strong> reads your configuration and looks for common issues. This should be
your first step when troubleshooting the system.<br />
<button id="btnSanity" class="btn btn-primary" onclick="sanity()"><i class="fa fa-info"></i> Sanity Check Your Installation</button>
</p>
<p style="border: 1px solid #eee">
<strong>Download</strong> support data to a local file. You can send this to LibreQoS via the
chat or email (as part of an ongoing support discussion).<br />
<button id="btnDownload" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#downloadDump"><i class="fa fa-download"></i> Download Support Dump</button>
</p>
<p style="border: 1px solid #eee">
<strong>Submit</strong> support data directly to LibreQoS. Please contact us when you do this,
with detailed information about the problems you are experiencing. Repeatedly hitting this
button will get you slower - or no - service!<br />
<button id="btnSubmit" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#submitDump"><i class="fa fa-send"></i> Send Support Dump</button>
</p>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<div class="modal" tabindex="-1" id="sanityModal">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configuration Check Results</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="overflow: auto;">
<p id="configCheck"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" tabindex="-1" id="downloadDump">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Information To Add to the Support Dump</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="overflow: auto;">
<div class="row">
<div class="col">
<label for="gatherName" class="form-label">Your Name</label>
<input type="text" id="gatherName" class="form-control" />
</div>
<div class="col">
<label for="gatherEmail" class="form-label">Your Email Address</label>
<input type="text" id="gatherEmail" class="form-control" />
</div>
</div>
<div class="row">
<div class="col">
<label for="gatherComments" class="form-label">Comments</label>
<input type="text" id="gatherComments" class="form-control" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="gather()"><i class="fa fa-download"></i> Generate and Download</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal" tabindex="-1" id="submitDump">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Information To Add to the Support Dump</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="overflow: auto;">
<p class="alert-warning"><i class="fa fa-warning"></i> Your support dump will contain unredated data about your customers, and lots
of information about your server. By submitting, you are acknowledging that LibreQoS bear no liability for this data.
LibreQoS will take all reasonable measures to protect your data, and will not share it.</p>
<div class="row">
<div class="col">
<label for="submitName" class="form-label">Your Name</label>
<input type="text" id="submitName" class="form-control" />
</div>
<div class="col">
<label for="submitEmail" class="form-label">Your Email Address</label>
<input type="text" id="submitEmail" class="form-control" />
</div>
</div>
<div class="row">
<div class="col">
<label for="submitComments" class="form-label">Comments</label>
<input type="text" id="submitComments" class="form-control" />
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" onclick="submit()"><i class="fa fa-send"></i> Submit Support Data to LibreQoS</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
function start() {
setTitle();
colorReloadButton();
updateHostCounts();
}
function gather() {
let details = {
name: $("#gatherName").val() + ", " + $("#gatherEmail").val(),
comment: $("#gatherComments").val()
};
console.log(details);
if (details.name === ", " || details.name.indexOf("@")<1) {
alert("Please enter a name and email address. If you share this, it'll be handy to know who sent it!");
return;
}
$.ajax({
type: "POST",
url: "/api/gatherSupport",
data: JSON.stringify(details),
xhrFields: {
responseType: 'blob' // to avoid binary data being mangled on charset conversion
},
success: function(blob, result, xhr) {
var filename = "libreqos.support";
// use HTML5 a[download] attribute to specify filename
var downloadUrl = URL.createObjectURL(blob);
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location.href = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
}
})
}
function submit() {
let details = {
name: $("#submitName").val() + ", " + $("#submitEmail").val(),
comment: $("#submitComments").val()
};
console.log(details);
if (details.name === ", " || details.name.indexOf("@")<1) {
alert("Please enter a name and email address. If you share this, it'll be handy to know who sent it!");
return;
}
$.ajax({
type: "POST",
url: "/api/submitSupport",
data: JSON.stringify(details),
success: function(result) {
console.log(result);
alert(result);
}
})
}
function sanity() {
$.get("/api/sanity", (data) => {
console.log(data);
let html = "<table class='table'><thead><th>Check</th><th>Success?</th><th>Comment</th></thead><tbody>";
for (let i=0; i<data.results.length; i++) {
let row = data.results[i];
html += "<tr>";
html += "<td>" + row.name + "</td>";
if (row.success) {
html += "<td style='color: green'><i class='fa fa-check'></i>";
} else {
html += "<td style='color: red'><i class='fa fa-warning'></i>";
}
html += "<td>" + row.comments + "</td>";
html += "</tr>";
}
html += "</tbody>";
$("#configCheck").html(html);
// Show the modal
const myModal = new bootstrap.Modal(document.getElementById('sanityModal'), { focus: true });
myModal.show();
})
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,376 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row">
<div class="col-sm-12">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-users"></i> Packet Dump</h5>
<div id="pages"></div>
<div id="graph"></div>
<div id="dump">Please Wait... this may take a second.</div>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
var packets = [];
var flows = {};
var pages = 0;
var PAGE_SIZE = 1000;
var target = "";
var capacity = [];
var activeFilter = null;
var activeSet = null;
var activeChart = 0;
var activePage = 0;
function filter(newFilter) {
activeFilter = newFilter;
if (newFilter == null) {
activeSet = packets;
} else {
activeSet = packets.filter(packet => packet.flow_id == activeFilter);
}
pages = Math.ceil((activeSet.length / PAGE_SIZE));
paginator(0);
viewPage(0);
}
function setChart(n) {
activeChart = n;
paginator(activePage);
viewPage(activePage);
}
function proto(n) {
switch (n) {
case 6: return "TCP"
case 17: return "UDP"
default: return "ICMP"
}
}
/*
Snippet for tcp_flags decoding
if (hdr->fin) flags |= 1;
if (hdr->syn) flags |= 2;
if (hdr->rst) flags |= 4;
if (hdr->psh) flags |= 8;
if (hdr->ack) flags |= 16;
if (hdr->urg) flags |= 32;
if (hdr->ece) flags |= 64;
if (hdr->cwr) flags |= 128;
*/
function tcp_flags(n) {
let result = "";
if (n & 1) result += "FIN-";
if (n & 2) result += "SYN-";
if (n & 4) result += "RST-";
if (n & 8) result += "PSH-";
if (n & 16) result += "ACK-";
if (n & 32) result += "URG-";
if (n & 64) result += "ECE-";
if (n & 128) result += "CWR-";
return result;
}
function zoomIn() {
PAGE_SIZE /= 2;
activePage /= 2;
pages = packets.length / PAGE_SIZE;
viewPage(activePage);
}
function zoomOut() {
PAGE_SIZE *= 2;
activePage *= 2;
pages = packets.length / PAGE_SIZE;
viewPage(activePage);
}
function paginator(active) {
activePage = active;
let paginator = "<a href='/api/pcap/" + target + "/capture-" + circuit_id + "-" + starting_timestamp + ".pcap' class='btn btn-warning'>Download PCAP Dump</a> ";
paginator += "<a href='#' class='btn btn-info' onClick='zoomIn();'>Zoom In</a> ";
paginator += "<a href='#' class='btn btn-info' onClick='zoomOut();'>Zoom Out</a> ( Or drag an area of the graph) <br />";
paginator += "<div style='margin: 4px; padding: 6px; background-color: #ddd; border: solid 1px black;'>";
paginator += "<strong>Jump to page</strong>: ";
paginator += "<select>"
for (let i=0; i<pages; i++) {
if (i == active) {
paginator += "<option selected>" + i + "</option>";
} else {
paginator += "<option onclick='viewPage(" + i + ");'>" + i + "</option> ";
}
}
paginator += "</select> | ";
// Offer flow filtering
paginator += "<strong>Filter Flows</strong>: ";
paginator += "<select>";
if (activeFilter == null) {
paginator += "<option selected onclick='filter(null);'>View All</option>";
} else {
paginator += "<option onclick='filter(null);'>View All</option>";
}
Object.keys(flows).forEach(key => {
if (activeFilter == key) {
paginator += "<option selected onclick='filter(\"" + key + "\");'>" + key + "</option>";
} else {
paginator += "<option onclick='filter(\"" + key + "\");'>" + key + "</option>";
}
});
paginator += "</select> | ";
// Offer graph choices
paginator += "<strong>Graph</strong>: ";
paginator += "<select>";
if (activeChart == 0) {
paginator += "<option selected>Packet-Size Chart</option>";
} else {
paginator += "<option onclick='setChart(0);'>Packet-Size Chart</option>";
}
if (activeChart == 1) {
paginator += "<option selected>Piano Roll Flow Chart</option>";
} else {
paginator += "<option onclick='setChart(1);'>Piano Roll Flow Chart</option>";
}
if (activeChart == 2) {
paginator += "<option selected>TCP Window Chart</option>";
} else {
paginator += "<option onclick='setChart(2);'>TCP Window Chart</option>";
}
paginator += "</select>";
paginator += "</div>";
$("#pages").html(paginator);
}
function viewPage(n) {
let start = n * PAGE_SIZE;
let end = Math.min(start + PAGE_SIZE, activeSet.length);
if (start > packets.length) {
console.log("OOps");
}
let html = "<table class='table table-striped'>";
html += "<thead><th>Time (nanos)</th><th>Proto</th><th>TCP Flags</th><th>Sequence</th><th>Window</th><th>Flow</th><th>Bytes</th><th>ECN</th><th>DSCP</th></thead>";
let x_axis = [];
let y1_axis = [];
let y2_axis = [];
for (let i=start; i<end; ++i) {
html += "<tr>";
html += "<td>" + activeSet[i].timestamp + "</td>";
html += "<td>" + proto(activeSet[i].ip_protocol) + "</td>";
if (activeSet[i].ip_protocol == 6) {
html += "<td>" + tcp_flags(activeSet[i].tcp_flags) + "</td>";
html += "<td>" + activeSet[i].tcp_tsval + "/" + activeSet[i].tcp_tsecr + "</td>";
html += "<td>" + activeSet[i].tcp_window + "</td>";
} else {
html += "<td></td><td></td><td></td>";
}
if (activeSet[i].ip_protocol != 1) {
html += "<td>" + activeSet[i].src + ":" + activeSet[i].src_port + " -> " + activeSet[i].dst + ":" + activeSet[i].dst_port + "</td>";
} else {
html += "<td>" + activeSet[i].src + " -> " + activeSet[i].dst + "</td>";
}
html += "<td>" + activeSet[i].size + "</td>";
html += "<td>" + ecn(activeSet[i].ecn) + "</td>";
html += "<td>0x" + activeSet[i].dscp.toString(16) + "</td>";
html += "</tr>";
x_axis.push(activeSet[i].timestamp);
if (activeSet[i].src == target) {
y1_axis.push(activeSet[i].size);
y2_axis.push(0);
} else {
y1_axis.push(0);
y2_axis.push(0.0 - activeSet[i].size);
}
}
html += "</table>";
$("#dump").html(html);
paginator(n);
// Make the graph
let graph = document.getElementById("graph");
if (activeChart == 0) {
// Render the timeline chart
let data = [
{x: x_axis, y:y1_axis, name: 'Download', type: 'scatter', mode: 'markers', error_x: { type: 'percent', value: capacity[0], symetric: false, valueminus: 0 }},
{x: x_axis, y:y2_axis, name: 'Upload', type: 'scatter', mode: 'markers', error_x: { type: 'percent', value: capacity[1], symetric: false, valueminus: 0 }},
];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: 'Bytes' }, xaxis: {automargin: true, title: "Nanoseconds"} }, { responsive: true });
} else if (activeChart == 1) {
// Render the piano roll chart
let flowGraphY = {};
for (var i=start; i<end; ++i) {
let flow_id = activeSet[i].flow_id;
if (flowGraphY.hasOwnProperty(flow_id)) {
flowGraphY[flow_id].x.push(activeSet[i].timestamp);
flowGraphY[flow_id].y.push(flows[flow_id].flowCounter);
} else {
flowGraphY[flow_id] = {
"x": [ activeSet[i].timestamp ],
"y": [ flows[flow_id].flowCounter ],
}
}
}
let data = [];
for (flow in flowGraphY) {
//console.log(flowGraphY[flow]);
data.push({
x: flowGraphY[flow].x, y: flowGraphY[flow].y, name: flow, type: 'scatter', mode: 'markers',
});
}
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: 'Flow' }, xaxis: {automargin: true, title: "Nanoseconds"} }, { responsive: true });
} else if (activeChart == 2) {
// Render the window chart
let flowGraphY = {};
for (var i=start; i<end; ++i) {
let flow_id = activeSet[i].flow_id;
if (flow_id.includes("TCP")) {
if (flowGraphY.hasOwnProperty(flow_id)) {
flowGraphY[flow_id].x.push(activeSet[i].timestamp);
flowGraphY[flow_id].y.push(activeSet[i].tcp_window);
} else {
flowGraphY[flow_id] = {
"x": [ activeSet[i].timestamp ],
"y": [ activeSet[i].tcp_window ],
}
}
}
}
let data = [];
for (flow in flowGraphY) {
//console.log(flowGraphY[flow]);
data.push({
x: flowGraphY[flow].x, y: flowGraphY[flow].y, name: flow, type: 'scatter', mode: 'markers',
});
}
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: 'Window Size' }, xaxis: {automargin: true, title: "Nanoseconds"} }, { responsive: true });
}
}
let circuit_id = null;
let starting_timestamp = null;
function start() {
colorReloadButton();
updateHostCounts();
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
circuit_id = params.circuit_id;
capacity = [ params.dn, params.up ]; // Bits per second
capacity = [ capacity[0] / 8, capacity[1] / 8 ]; // Bytes per second
capacity = [ capacity[0] / 1e9, capacity[1] / 1e9 ]; // Bytes per nanosecond
target = params.id;
$.get("/api/packet_dump/" + params.id, (data) => {
console.log(data);
data.sort((a,b) => a.timestamp - b.timestamp);
// Find the minimum timestamp
let min_ts = data.reduce((prev, curr) => prev.timestamp < curr.timestamp ? prev : curr).timestamp;
// Set the displayed timestamp to be (ts - min)
data.forEach(packet => packet.timestamp -= min_ts);
// Divide the packets into flows and append the flow_id
let flowCounter = 0;
data.forEach(packet => {
let flow_id = proto(packet.ip_protocol) + " " + packet.src + ":" + packet.src_port + " <-> " + packet.dst + ":" + packet.dst_port;
let reverse_flow_id = proto(packet.ip_protocol) + " " + packet.dst + ":" + packet.dst_port + " <-> " + packet.src + ":" + packet.src_port;
if (flows.hasOwnProperty(flow_id)) {
packet.flow_id = flow_id;
} else if (flows.hasOwnProperty(reverse_flow_id)) {
packet.flow_id = reverse_flow_id;
} else {
flows[flow_id] = { flowCounter };
packet.flow_id = flow_id;
flowCounter++;
}
});
packets = data;
activeSet = packets;
pages = Math.ceil((activeSet.length / PAGE_SIZE));
starting_timestamp = min_ts;
paginator(0);
viewPage(0);
});
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,83 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<div id="container" class="pad4">
<div class="row">
<div class="col-sm-4"></div>
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title">Login</h5>
<p>Please enter a username and password to access LibreQoS.</p>
<p>You can control access locally with <em>bin/lqusers</em> from the console.</p>
<table class="table">
<tr>
<td>Username</td>
<td><input type="text" id="username" /></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" id="password" /></td>
</tr>
</table>
<a class="btn btn-primary" id="btnLogin">Login</a>
</div>
</div>
</div>
<div class="col-sm-4"></div>
</div>
</div>
<footer>
<a href="https://libreqos.io/credits/">&copy; 2022 - 2023, LibreQoE LLC</a>
</footer>
<script>
function try_login() {
let newUser = {
username: $("#username").val(),
password: $("#password").val(),
};
$.ajax({
type: "POST",
url: "/api/login",
data: JSON.stringify(newUser),
success: (data) => {
if (data == "ERROR") {
alert("Invalid login")
} else {
window.location.href = "/";
}
}
})
}
function start() {
$("#btnLogin").on('click', try_login)
$(document).on('keydown', (e) => {
if (e.keyCode === 13) {
try_login()
}
})
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,22 +0,0 @@
@font-face {
font-family: Klingon;
src: url(/vendor/klingon.ttf);
}
.green-badge { background-color: green; }
.orange-badge { background-color: darkgoldenrod; }
.black-txt { color: black; }
.pad4 { padding: 4px; }
.top-shunt { margin-top: -4px; margin-bottom: 4px; }
.center-txt { text-align: center; }
.bold { font-weight: bold; }
.graph200 { height: 200px; }
.graph150 { height: 150px; }
.graph98 { min-height: 97px; width: 100%; }
.mtop4 { margin-top: 4px; }
.mbot4 { margin-bottom: 4px; }
.mbot8 { margin-bottom: 8px; }
.row220 { height: 220px; }
.redact { font-display: unset; }
footer > a { color: white; }
footer { color: white; font-style: italic; }
.invalid { background-color: #ffdddd }

View File

@ -1,513 +0,0 @@
function msgPackGet(url, success) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "arraybuffer";
xhr.onload = () => {
var data = xhr.response;
let decoded = msgpack.decode(new Uint8Array(data));
success(decoded);
};
xhr.send(null);
}
const NetTrans = {
"name": 0,
"max_throughput": 1,
"current_throughput": 2,
"rtts": 3,
"parents": 4,
"immediate_parent": 5
}
const Circuit = {
"id" : 0,
"name" : 1,
"traffic": 2,
"limit": 3,
}
const IpStats = {
"ip_address": 0,
"bits_per_second": 1,
"packets_per_second": 2,
"median_tcp_rtt": 3,
"tc_handle": 4,
"circuit_id": 5,
"plan": 6,
"tcp_retransmits": 7,
}
const FlowTrans = {
"src": 0,
"dst": 1,
"proto": 2,
"src_port": 3,
"dst_port": 4,
"bytes": 5,
"packets": 6,
"dscp": 7,
"ecn": 8
}
const CircuitInfo = {
"name" : 0,
"capacity" : 1,
}
const QD = { // Queue data
"history": 0,
"history_head": 1,
"current_download": 2,
"current_upload": 3,
}
const CT = { // Cake transit
"memory_used": 0,
}
const CDT = { // Cake Diff Transit
"bytes": 0,
"packets": 1,
"qlen": 2,
"tins": 3,
}
const CDTT = { // Cake Diff Tin Transit
"sent_bytes": 0,
"backlog_bytes": 1,
"drops": 2,
"marks": 3,
"avg_delay_us": 4,
}
function metaverse_color_ramp(n) {
if (n <= 9) {
return "#32b08c";
} else if (n <= 20) {
return "#ffb94a";
} else if (n <= 50) {
return "#f95f53";
} else if (n <= 70) {
return "#bf3d5e";
} else {
return "#dc4e58";
}
}
function regular_color_ramp(n) {
if (n <= 100) {
return "#aaffaa";
} else if (n <= 150) {
return "goldenrod";
} else {
return "#ffaaaa";
}
}
function color_ramp(n) {
let colorPreference = window.localStorage.getItem("colorPreference");
if (colorPreference == null) {
window.localStorage.setItem("colorPreference", 0);
colorPreference = 0;
}
if (colorPreference == 0) {
return regular_color_ramp(n);
} else {
return metaverse_color_ramp(n);
}
}
function deleteAllCookies() {
const cookies = document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const eqPos = cookie.indexOf("=");
const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
}
window.location.reload();
}
function cssrules() {
var rules = {};
for (var i = 0; i < document.styleSheets.length; ++i) {
var cssRules = document.styleSheets[i].cssRules;
for (var j = 0; j < cssRules.length; ++j)
rules[cssRules[j].selectorText] = cssRules[j];
}
return rules;
}
function css_getclass(name) {
var rules = cssrules();
if (!rules.hasOwnProperty(name))
throw 'TODO: deal_with_notfound_case';
return rules[name];
}
function updateHostCounts() {
msgPackGet("/api/host_counts", (hc) => {
$("#shapedCount").text(hc[0]);
$("#unshapedCount").text(hc[1]);
setTimeout(updateHostCounts, 5000);
});
$.get("/api/username", (un) => {
let html = "";
if (un == "Anonymous") {
html = "<a class='nav-link' href='/login'><i class='fa fa-user'></i> Login</a>";
} else {
html = "<a class='nav-link' onclick='deleteAllCookies();'><i class='fa fa-user'></i> Logout " + un + "</a>";
}
$("#currentLogin").html(html);
});
/*$("#startTest").on('click', () => {
$.get("/api/run_btest", () => { });
});*/
// LTS Check
$.get("/api/stats_check", (data) => {
//console.log(data);
let template = "<a class='nav-link' href='$URL$'><i class='fa fa-dashboard'></i> $TEXT$</a>";
switch (data.action) {
case "Disabled": {
template = template.replace("$URL$", "#")
.replace("$TEXT$", "<span style='color: red'>Stats Disabled</span>");
}
case "NotSetup": {
template = template.replace("$URL$", "https://stats.libreqos.io/trial1/" + encodeURI(data.node_id))
.replace("$TEXT$", "<span class='badge badge-pill badge-success green-badge'>Statistics Free Trial</span>");
} break;
default: {
template = template.replace("$URL$", "https://stats.libreqos.io/")
.replace("$TEXT$", "Statistics");
}
}
$("#statsLink").html(template);
});
}
function colorReloadButton() {
$("body").append(reloadModal);
$("#btnReload").on('click', () => {
$.get("/api/reload_libreqos", (result) => {
const myModal = new bootstrap.Modal(document.getElementById('reloadModal'), { focus: true });
$("#reloadLibreResult").text(result);
myModal.show();
});
});
$.get("/api/reload_required", (req) => {
if (req) {
$("#btnReload").addClass('btn-warning');
$("#btnReload").css('color', 'darkred');
} else {
$("#btnReload").addClass('btn-secondary');
}
});
// Redaction
if (isRedacted()) {
console.log("Redacting");
//css_getclass(".redact").style.filter = "blur(4px)";
css_getclass(".redact").style.fontFamily = "klingon";
}
}
function isRedacted() {
let redact = localStorage.getItem("redact");
if (redact == null) {
localStorage.setItem("redact", false);
redact = false;
}
if (redact == "false") {
redact = false;
} else if (redact == "true") {
redact = true;
}
return redact;
}
const phrases = [
"quSDaq balua", // Is this seat taken?
"vjIjatlh", // speak
"pevIl muqaDmey", // curse well
"nuqDaq oH puchpae", // where's the bathroom?
"nuqDaq oH tache", // Where's the bar?
"terangan Soj lujaba", // Do they serve Earth food?
"qut na HInob", // Give me the salty crystals
"qagh Sopbe", // He doesn't eat gagh
"HIja", // Yes
"ghobe", // No
"Dochvetlh vIneH", // I want that thing
"Hab SoSlI Quch", // Your mother has a smooth forehead
"nuqjatlh", // What did you say?
"jagh yIbuStaH", // Concentrate on the enemy
"HeghlumeH QaQ jajvam", // Today is a good day to die
"qaStaH nuq jay", // WTF is happening?
"wo batlhvaD", // For the honor of the empire
"tlhIngan maH", // We are Klingon!
"Qapla", // Success!
]
function redactText(text) {
if (!isRedacted()) return text;
let redacted = "";
let sum = 0;
for (let i = 0; i < text.length; i++) {
let code = text.charCodeAt(i);
sum += code;
}
sum = sum % phrases.length;
return phrases[sum];
}
function scaleNumber(n) {
if (n > 1000000000000) {
return (n / 1000000000000).toFixed(2) + "T";
} else if (n > 1000000000) {
return (n / 1000000000).toFixed(2) + "G";
} else if (n > 1000000) {
return (n / 1000000).toFixed(2) + "M";
} else if (n > 1000) {
return (n / 1000).toFixed(2) + "K";
}
return n;
}
function scaleNanos(n) {
if (n == 0) return "";
if (n > 1000000000) {
return (n / 1000000000).toFixed(2) + "s";
} else if (n > 1000000) {
return (n / 1000000).toFixed(2) + "ms";
} else if (n > 1000) {
return (n / 1000).toFixed(2) + "µs";
}
return n + "ns";
}
const reloadModal = `
<div class='modal fade' id='reloadModal' tabindex='-1' aria-labelledby='reloadModalLabel' aria-hidden='true'>
<div class='modal-dialog modal-fullscreen'>
<div class='modal-content'>
<div class='modal-header'>
<h1 class='modal-title fs-5' id='reloadModalLabel'>LibreQoS Reload Result</h1>
<button type='button' class='btn-close' data-bs-dismiss='modal' aria-label='Close'></button>
</div>
<div class='modal-body'>
<pre id='reloadLibreResult' style='overflow: vertical; height: 100%; width: 100%;'>
</pre>
</div>
<div class='modal-footer'>
<button type='button' class='btn btn-secondary' data-bs-dismiss='modal'>Close</button>
</div>
</div>
</div>
</div>`;
// MultiRingBuffer provides an interface for storing multiple ring-buffers
// of performance data, with a view to them ending up on the same graph.
class MultiRingBuffer {
constructor(capacity) {
this.capacity = capacity;
this.data = {};
}
push(id, download, upload) {
if (!this.data.hasOwnProperty(id)) {
this.data[id] = new RingBuffer(this.capacity);
}
this.data[id].push(download, upload);
}
plotStackedBars(target_div, rootName) {
let graphData = [];
for (const [k, v] of Object.entries(this.data)) {
if (k != rootName) {
let y = v.sortedY;
let dn = { x: v.x_axis, y: y.down, name: k + "_DL", type: 'scatter', stackgroup: 'dn' };
let up = { x: v.x_axis, y: y.up, name: k + "_UL", type: 'scatter', stackgroup: 'up' };
graphData.push(dn);
graphData.push(up);
}
}
let graph = document.getElementById(target_div);
Plotly.newPlot(
graph,
graphData,
{
margin: { l: 0, r: 0, b: 0, t: 0, pad: 4 },
yaxis: { automargin: true },
xaxis: { automargin: true, title: "Time since now (seconds)" },
showlegend: false,
},
{ responsive: true, displayModeBar: false });
}
plotTotalThroughput(target_div) {
let graph = document.getElementById(target_div);
this.data['total'].prepare();
this.data['shaped'].prepare();
let x = this.data['total'].x_axis;
let graphData = [
{x: x, y:this.data['total'].sortedY[0], name: 'Download', type: 'scatter', marker: {color: 'rgb(255,160,122)'}},
{x: x, y:this.data['total'].sortedY[1], name: 'Upload', type: 'scatter', marker: {color: 'rgb(255,160,122)'}},
{x: x, y:this.data['shaped'].sortedY[0], name: 'Shaped Download', type: 'scatter', fill: 'tozeroy', marker: {color: 'rgb(124,252,0)'}},
{x: x, y:this.data['shaped'].sortedY[1], name: 'Shaped Upload', type: 'scatter', fill: 'tozeroy', marker: {color: 'rgb(124,252,0)'}},
];
if (this.plotted == null) {
Plotly.newPlot(
graph,
graphData,
{
margin: { l:0,r:0,b:0,t:0,pad:4 },
yaxis: { automargin: true, title: "Traffic (bits)", exponentformat: "SI" },
xaxis: {automargin: true, title: "Time since now (seconds)"}
}, { responsive: true });
this.plotted = true;
} else {
Plotly.redraw(graph, graphData);
}
}
}
class RingBuffer {
constructor(capacity) {
this.capacity = capacity;
this.head = capacity - 1;
this.download = [];
this.upload = [];
this.x_axis = [];
this.sortedY = [ [], [] ];
for (var i = 0; i < capacity; ++i) {
this.download.push(0.0);
this.upload.push(0.0);
this.x_axis.push(capacity - i);
this.sortedY[0].push(0);
this.sortedY[1].push(0);
}
}
push(download, upload) {
this.download[this.head] = download;
this.upload[this.head] = 0.0 - upload;
this.head += 1;
this.head %= this.capacity;
}
prepare() {
let counter = 0;
for (let i=this.head; i<this.capacity; i++) {
this.sortedY[0][counter] = this.download[i];
this.sortedY[1][counter] = this.upload[i];
counter++;
}
for (let i=0; i < this.head; i++) {
this.sortedY[0][counter] = this.download[i];
this.sortedY[1][counter] = this.upload[i];
counter++;
}
}
toScatterGraphData() {
this.prepare();
let GraphData = [
{ x: this.x_axis, y: this.sortedY[0], name: 'Download', type: 'scatter' },
{ x: this.x_axis, y: this.sortedY[1], name: 'Upload', type: 'scatter' },
];
return GraphData;
}
}
class RttHistogram {
constructor() {
this.entries = []
this.x = [];
for (let i = 0; i < 20; ++i) {
this.entries.push(i);
this.x.push(i * 10);
}
}
clear() {
for (let i = 0; i < 20; ++i) {
this.entries[i] = 0;
}
}
push(rtt) {
let band = Math.floor(rtt / 10.0);
if (band > 19) {
band = 19;
}
this.entries[band] += 1;
}
pushBand(band, n) {
this.entries[band] += n;
}
plot(target_div) {
let gData = [
{ x: this.x, y: this.entries, type: 'bar', marker: { color: this.x, colorscale: 'RdBu' } }
]
let graph = document.getElementById(target_div);
if (this.plotted == null) {
Plotly.newPlot(graph, gData, { margin: { l: 40, r: 0, b: 35, t: 0 }, yaxis: { title: "# Hosts" }, xaxis: { title: 'TCP Round-Trip Time (ms)' } }, { responsive: true });
this.plotted = true;
} else {
Plotly.redraw(graph, gData);
}
}
}
function ecn(n) {
switch (n) {
case 0: return "-";
case 1: return "L4S";
case 2: return "ECT0";
case 3: return "CE";
default: return "???";
}
}
function zip(a, b) {
let zipped = [];
for (let i=0; i<a.length; ++i) {
zipped.push(a[i]);
zipped.push(b[i]);
}
return zipped;
}
function zero_to_null(array) {
for (let i=0; i<array.length; ++i) {
if (array[i] == 0) array[i] = null;
}
}
var dnsCache = {};
function ipToHostname(ip) {
if (dnsCache.hasOwnProperty(ip)) {
if (dnsCache[ip] != ip) {
return ip + "<br /><span style='font-size: 6pt'>" + dnsCache[ip] + "</span>";
} else {
return ip;
}
}
$.get("/api/dns/" + encodeURI(ip), (hostname) => {
dnsCache[ip] = hostname;
})
return ip;
}
function setTitle() {
$.get("/api/node_name", (name) => {
// Set the window title
document.title = name + " - LibreQoS Node Manager";
})
}

View File

@ -1,600 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div id="toasts"></div>
<!-- Dashboard Row 1 -->
<div class="row mbot8">
<!-- THROUGHPUT -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bolt"></i> Current Throughput <span class="badge badge-pill green-badge" id="flowCount">?</span></h5>
<table class="table">
<tr>
<td class="bold">Packets/Second</td>
<td id="ppsDown"></td>
<td id="ppsUp"></td>
</tr>
<tr>
<td class="bold">Bits/Second</td>
<td id="bpsDown"></td>
<td id="bpsUp"></td>
</tr>
</table>
</div>
</div>
</div>
<!-- RAM INFO -->
<div class="col-sm-2">
<div class="card bg-light d-none d-lg-block">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-database"></i> Memory Status</h5>
<div id="ram" class="graph98"></div>
</div>
</div>
</div>
<!-- CPU INFO -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-microchip"></i> CPU Status</h5>
<div id="cpu" class="graph98"></div>
</div>
</div>
</div>
</div>
<!-- Dashboard Row 2 -->
<div class="row mbot8 row220">
<!-- 5 minutes of throughput -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-dashboard"></i> Last 5 Minutes</h5>
<div id="tpGraph" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- RTT Histogram -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
<div id="rttHistogram" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- Site Funnel -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-tree"></i> Network Tree</h5>
<div id="siteFunnel" class="graph98 graph150"></div>
</div>
</div>
</div>
</div>
<!-- Dashboard Row 3 -->
<div class="row">
<!-- Top 10 downloaders -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title">
<i class='fa fa-arrow-down'></i> Top 10 Downloaders
<button id="btntop10dl" class="btn btn-small btn-success" href="/top10" onclick="showCircuits()">Circuits</button>
<button id="btntop10flows" class="btn btn-small btn-primary" href="/top10" onclick="showFlows()">Flows</button>
<button id="btntop10ep" class="btn btn-small btn-primary" href="/top10" onclick="showEndpoints()">Geo Endpoints</button>
<button id="btntop10pro" class="btn btn-small btn-primary" href="/top10" onclick="showProtocols()">Protocols</button>
<button id="btntop10eth" class="btn btn-small btn-primary" href="/top10" onclick="showEthertypes()">Ethertypes</button>
<a href="/showoff" class="btn btn-small btn-info"><i class="fa-solid fa-map"></i> Flow Map</a>
</h5>
<div id="top10dl" style="display:block;"></div>
<div id="top10flows" style="display: none;"></div>
<div id="top10ep" style="display: none;"></div>
<div id="top10eth" style="display: none;"></div>
<div id="top10pro" style="display: none;"></div>
</div>
</div>
</div>
<!-- Worst 10 RTT -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class='fa fa-exclamation'></i> Worst 10
<button id="btnworstRtt" class="btn btn-small btn-success" href="/top10" onclick="showWorstRtt()">RTT</button>
<button id="btnworstTcp" class="btn btn-small btn-primary" href="/top10" onclick="showWorstTcp()">TCP Retransmits</button>
</h5>
<div id="worstRtt"></div>
<div id="worstTcp" style="display: none;"></div>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
var throughput = new MultiRingBuffer(300);
// Loads the complete ringbuffer for initial display
function fillCurrentThroughput() {
msgPackGet("/api/throughput_ring_buffer", (tp) => {
//console.log(tp);
const bits = 0;
const packets = 1;
const shaped = 2;
let head = tp[0];
for (let i=head; i<300; ++i) {
throughput.push("pps", tp[1][i][packets][0], tp[1][i][packets][1]);
throughput.push("total", tp[1][i][bits][0], tp[1][i][bits][1]);
throughput.push("shaped", tp[1][i][shaped][0], tp[1][i][shaped][1]);
}
for (let i=0; i<head; ++i) {
throughput.push("pps", tp[1][i][packets][0], tp[1][i][packets][1]);
throughput.push("total", tp[1][i][bits][0], tp[1][i][bits][1]);
throughput.push("shaped", tp[1][i][shaped][0], tp[1][i][shaped][1]);
}
throughput.plotTotalThroughput("tpGraph");
});
}
function updateFlowCounter() {
$.get("/api/flows/count", (data) => {
$("#flowCount").text(data + " flows");
});
}
function updateCurrentThroughput() {
msgPackGet("/api/current_throughput", (tp) => {
const bits = 0;
const packets = 1;
const shaped = 2;
$("#ppsDown").text(scaleNumber(tp[packets][0]));
$("#ppsUp").text(scaleNumber(tp[packets][1]));
$("#bpsDown").text(scaleNumber(tp[bits][0]));
$("#bpsUp").text(scaleNumber(tp[bits][1]));
throughput.push("pps", tp[packets][0], tp[packets][1]);
throughput.push("total", tp[bits][0], tp[bits][1]);
throughput.push("shaped", tp[shaped][0], tp[shaped][1]);
throughput.plotTotalThroughput("tpGraph");
});
}
var funnelData = new MultiRingBuffer(300);
function updateSiteFunnel() {
msgPackGet("/api/network_tree_summary/", (data) => {
let table = "<table class='table table-striped' style='font-size: 8pt;'>";
for (let i = 0; i < data.length; ++i) {
let id = data[i][0];
let name = data[i][1][NetTrans.name];
if (name.length > 20) {
name = name.substring(0, 20) + "...";
}
table += "<tr>";
table += "<td class='redact'><a href='/tree?parent=" + id + "'>" + redactText(name) + "</a></td>";
table += "<td>" + scaleNumber(data[i][1][NetTrans.current_throughput][0] * 8) + "</td>";
table += "<td>" + scaleNumber(data[i][1][NetTrans.current_throughput][1] * 8) + "</td>";
table += "</tr>";
}
table += "</table>";
$("#siteFunnel").html(table);
});
}
function updateCpu() {
msgPackGet("/api/cpu", (cpu) => {
let graph = document.getElementById("cpu");
let x = [];
let y = [];
let colors = [];
for (i = 0; i < cpu.length; i++) {
x.push(i);
y.push(cpu[i]);
colors.push(cpu[i]);
}
colors.push(100); // 1 extra colors entry to force color scaling
let data = [{ x: x, y: y, type: 'bar', marker: { color: colors, colorscale: 'Jet' } }];
Plotly.newPlot(graph, data, {
margin: { l: 0, r: 0, b: 15, t: 0 },
yaxis: { automargin: true, autorange: false, range: [0.0, 100.0] },
},
{ responsive: true });
});
}
function updateRam() {
msgPackGet("/api/ram", (ram) => {
let graph = document.getElementById("ram");
let data = [{
values: [Math.round(ram[0]), Math.round(ram[1] - ram[0])],
labels: ['Used', 'Available'],
type: 'pie'
}];
Plotly.newPlot(graph, data, { margin: { l: 0, r: 0, b: 0, t: 12 }, showlegend: false }, { responsive: true });
});
}
function updateNTable(target, tt) {
let html = "<table class='table table-striped' style='font-size: 8pt'>";
html += "<thead><th></th><th>IP Address</th><th>DL ⬇️</th><th>UL ⬆️</th><th>RTT (ms)</th><th>TCP Retransmits</th><th>Shaped</th></thead>";
for (let i = 0; i < tt.length; i++) {
let color = color_ramp(tt[i][IpStats.median_tcp_rtt]);
html += "<tr>";
html += "<td style='color: " + color + "'></td>";
if (tt[i][IpStats.circuit_id] != "") {
html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(tt[i][IpStats.circuit_id]) + "'>" + redactText(tt[i][IpStats.ip_address]) + "</td>";
} else {
html += "<td><span class='redact'>" + redactText(tt[i][IpStats.ip_address]) + "</span></td>";
}
html += "<td>" + scaleNumber(tt[i][IpStats.bits_per_second][0]) + "</td>";
html += "<td>" + scaleNumber(tt[i][IpStats.bits_per_second][1]) + "</td>";
html += "<td>" + tt[i][IpStats.median_tcp_rtt].toFixed(2) + "</td>";
html += "<td>" + tt[i][IpStats.tcp_retransmits][0] + "/" + tt[i][IpStats.tcp_retransmits][1] + "</td>";
if (tt[i].tc_handle != 0) {
html += "<td><i class='fa fa-check-circle'></i> (" + tt[i][IpStats.plan][0] + "/" + tt[i][IpStats.plan][1] + ")</td>";
} else {
//html += "<td><a class='btn btn-small btn-success' href='/shaped-add?ip=" + tt[i].ip_address + "'>Add Shaper</a></td>";
html += "<td>Not Shaped</td>"
}
html += "</tr>";
}
html += "</table>";
$(target).html(html);
}
function updateTop10() {
msgPackGet("/api/top_10_downloaders", (tt) => {
updateNTable('#top10dl', tt);
});
}
function updateWorst10() {
msgPackGet("/api/worst_10_rtt", (tt) => {
updateNTable('#worstRtt', tt);
});
}
function updateWorstTcp() {
msgPackGet("/api/worst_10_tcp", (tt) => {
//console.log(tt);
updateNTable('#worstTcp', tt);
});
}
function updateTop10Flows() {
$.get("/api/flows/top/10/rate", data => {
let html = "<table class='table table-striped' style='font-size: 8pt'>";
html += "<thead>";
html += "<th>Protocol</th>";
html += "<th>Local IP</th>";
html += "<th>Remote IP</th>";
html += "<th>UL ⬆️</th>";
html += "<th>DL ⬇️</th>";
html += "<th>UL RTT</th>";
html += "<th>DL RTT</th>";
html += "<th>TCP Retransmits</th>";
html += "<th>Remote ASN</th>";
html += "<th>Country</th>";
html += "</thead><tbody>";
for (var i = 0; i<data.length; i++) {
//console.log(data[i]);
html += "<tr>";
html += "<td>" + data[i].analysis + "</td>";
html += "<td>" + data[i].local_ip + "</td>";
html += "<td>" + data[i].remote_ip + "</td>";
// TODO: Check scaling
html += "<td>" + scaleNumber(data[i].rate_estimate_bps[0]) + "</td>";
html += "<td>" + scaleNumber(data[i].rate_estimate_bps[1]) + "</td>";
html += "<td>" + scaleNanos(data[i].rtt_nanos[0]) + "</td>";
html += "<td>" + scaleNanos(data[i].rtt_nanos[1]) + "</td>";
html += "<td>" + data[i].tcp_retransmits[0] + "/" + data[i].tcp_retransmits[1] + "</td>";
html += "<td>" + data[i].remote_asn_name + "</td>";
html += "<td>" + data[i].remote_asn_country + "</td>";
html += "</tr>";
}
html += "</tbody></table>";
$("#top10flows").html(html);
});
}
function updateTop10Endpoints() {
$.get("/api/flows/by_country", data => {
//console.log(data);
let html = "<table class='table table-striped' style='font-size: 8pt'>";
html += "<thead>";
html += "<th>Country</th>";
html += "<th>UL ⬆️</th>";
html += "<th>DL ⬇️</th>";
html += "<th>UL RTT</th>";
html += "<th>DL RTT</th>";
html += "</thead></tbody>";
let i = 0;
while (i < data.length && i < 10) {
html += "<tr>";
html += "<td>" + data[i][0] + "</td>";
html += "<td>" + scaleNumber(data[i][1][0]) + "</td>";
html += "<td>" + scaleNumber(data[i][1][1]) + "</td>";
html += "<td>" + scaleNanos(data[i][2][0]) + "</td>";
html += "<td>" + scaleNanos(data[i][2][1]) + "</td>";
html += "</tr>";
i += 1;
}
html += "</tbody></table>";
$("#top10ep").html(html);
});
}
function updateTop10Ethertypes() {
$.get("/api/flows/ether_protocol", data => {
let html = "<table class='table' style='font-size: 8pt'>";
html += "<thead>";
html += "<th>Protocol</th>";
html += "<th>UL ⬆️</th>";
html += "<th>DL ⬇️</th>";
html += "<th>UL RTT</th>";
html += "<th>DL RTT</th>";
html += "</thead></tbody>";
let row = data.EtherProtocols;
html += "<tr>";
html += "<td>IPv4</td>";
html += "<td>" + scaleNumber(row.v4_bytes[0]) + "</td>";
html += "<td>" + scaleNumber(row.v4_bytes[1]) + "</td>";
html += "<td>" + scaleNanos(row.v4_rtt[0]) + "</td>";
html += "<td>" + scaleNanos(row.v4_rtt[1]) + "</td>";
html += "</tr>";
html += "<tr>";
html += "<td>IPv6</td>";
html += "<td>" + scaleNumber(row.v6_bytes[0]) + "</td>";
html += "<td>" + scaleNumber(row.v6_bytes[1]) + "</td>";
html += "<td>" + scaleNanos(row.v6_rtt[0]) + "</td>";
html += "<td>" + scaleNanos(row.v6_rtt[1]) + "</td>";
html += "</tr>";
html += "</tbody></table>";
$("#top10eth").html(html);
});
}
function updateTop10Protocols() {
$.get("/api/flows/ip_protocol", data => {
let html = "<table class='table' style='font-size: 8pt'>";
html += "<thead>";
html += "<th>Protocol</th>";
html += "<th>UL ⬆️</th>";
html += "<th>DL ⬇️</th>";
html += "</thead></tbody>";
for (i=0; i<data.length; i++) {
html += "<tr>";
html += "<td>" + data[i][0] + "</td>";
html += "<td>" + scaleNumber(data[i][1][0]) + "</td>";
html += "<td>" + scaleNumber(data[i][1][1]) + "</td>";
html += "</tr>";
}
html += "</tbody></table>";
$("#top10pro").html(html);
});
}
let top10view = "circuits";
let worst10view = "rtt";
function changeBottom10(visible) {
const bottom10 = ["worstRtt", "worstTcp"];
for (let i=0; i<bottom10.length; i++) {
$("#" + bottom10[i]).hide();
$("#btn" + bottom10[i]).removeClass("btn-success");
$("#btn" + bottom10[i]).addClass("btn-primary");
}
$("#" + visible).show();
$("#btn" + visible).removeClass("btn-primary");
$("#btn" + visible).addClass("btn-success");
}
function showWorstRtt() {
changeBottom10("worstRtt");
worst10view = "rtt";
}
function showWorstTcp() {
changeBottom10("worstTcp");
worst10view = "tcp";
}
function changeTop10(visible) {
const top10 = ["top10dl", "top10flows", "top10ep", "top10eth", "top10pro"];
for (let i=0; i<top10.length; i++) {
$("#" + top10[i]).hide();
$("#btn" + top10[i]).removeClass("btn-success");
$("#btn" + top10[i]).addClass("btn-primary");
}
$("#" + visible).show();
$("#btn" + visible).removeClass("btn-primary");
$("#btn" + visible).addClass("btn-success");
}
function showCircuits() {
changeTop10("top10dl");
top10view = "circuits";
}
function showFlows() {
changeTop10("top10flows");
top10view = "flows";
}
function showEndpoints() {
changeTop10("top10ep");
top10view = "endpoints";
}
function showProtocols() {
changeTop10("top10pro");
top10view = "protocols";
}
function showEthertypes() {
changeTop10("top10eth");
top10view = "ethertypes";
}
var rttGraph = new RttHistogram();
function updateHistogram() {
msgPackGet("/api/rtt_histogram", (rtt) => {
rttGraph.clear();
for (let i = 0; i < rtt.length; i++) {
rttGraph.pushBand(i, rtt[i]);
}
rttGraph.plot("rttHistogram");
});
}
var tickCount = 0;
function OneSecondCadence() {
updateCurrentThroughput();
updateFlowCounter();
updateSiteFunnel();
if (tickCount % 5 == 0) {
updateHistogram();
if (worst10view == "rtt") {
updateWorst10();
} else if (worst10view == "tcp") {
updateWorstTcp();
}
if (top10view == "circuits") {
updateTop10();
} else if (top10view == "flows") {
updateTop10Flows();
} else if (top10view == "endpoints") {
updateTop10Endpoints();
} else if (top10view == "protocols") {
updateTop10Protocols();
} else if (top10view == "ethertypes") {
updateTop10Ethertypes();
}
}
if (tickCount % 10 == 0) {
updateCpu();
updateRam();
}
tickCount++;
setTimeout(OneSecondCadence, 1000);
}
function start() {
setTitle();
if (isRedacted()) {
//console.log("Redacting");
//css_getclass(".redact").style.filter = "blur(4px)";
css_getclass(".redact").style.fontFamily = "klingon";
}
colorReloadButton();
fillCurrentThroughput();
updateFlowCounter();
updateCpu();
updateRam();
updateTop10();
updateWorst10();
updateHistogram();
updateHostCounts();
updateSiteFunnel();
OneSecondCadence();
// Version Check
$.get("/api/version_check", (data) => {
if (data != "All Good") {
let html = "<div class='alert alert-info alert-dismissible fade show' role='alert'>";
html += "<strong>LibreQoS Update Available!</strong>";
html += "<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>";
html += "</div>";
$("#toasts").append(html);
}
});
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,167 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row">
<div class="col-sm-12">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-users"></i> Add Shaped Circuit</h5>
<div class="row">
<div class="col">
<label for="circuitId" class="form-label">Circuit ID</label>
<input type="text" id="circuitId" class="form-control" />
</div>
<div class="col">
<label for="circuitName" class="form-label">Circuit Name</label>
<input type="text" id="circuitName" class="form-control" />
</div>
</div>
<div class="row">
<div class="col">
<label for="deviceId" class="form-label">Device ID</label>
<input type="text" id="deviceId" class="form-control" />
</div>
<div class="col">
<label for="circuitName" class="form-label">Device Name</label>
<input type="text" id="deviceName" class="form-control" />
</div>
<div class="col">
<label for="parent" class="form-label">Parent</label>
<input type="text" id="parent" class="form-control" />
</div>
<div class="col">
<label for="mac" class="form-label">MAC Address</label>
<input type="text" id="mac" class="form-control" />
</div>
</div>
<div class="row">
<div class="col">
<label for="dlMin" class="form-label">Download Minimum (Mbps)</label>
<input type="number" id="dlMin" class="form-control" />
</div>
<div class="col">
<label for="ulMin" class="form-label">Upload Minimum (Mbps)</label>
<input type="number" id="ulMin" class="form-control" />
</div>
<div class="col">
<label for="dlMax" class="form-label">Download Maximum (Mbps)</label>
<input type="number" id="dlMax" class="form-control" />
</div>
<div class="col">
<label for="ulMax" class="form-label">Upload Maximum (Mbps)</label>
<input type="number" id="ulMax" class="form-control" />
</div>
</div>
<div class="row mbot8">
<div class="col">
<label for="comment" class="form-label">Comment</label>
<input type="text" id="comment" class="form-control" />
</div>
</div>
<div class="row mbot8">
<div class="col">
<strong>IPv4 Addresses</strong> (You can use 1.2.3.4/X to match a CIDR subnet)<br />
<label for="ipv4_1" class="form-label">Address 1</label>
<input type="text" id="ipv4_1" class="form-control" />
<label for="ipv4_2" class="form-label">Address 2</label>
<input type="text" id="ipv4_2" class="form-control" />
<label for="ipv4_3" class="form-label">Address 3</label>
<input type="text" id="ipv4_3" class="form-control" />
</div>
<div class="col">
<strong>IPv6 Addresses</strong> (You can use /X to match a subnet)<br />
<label for="ipv6_1" class="form-label">Address 1</label>
<input type="text" id="ipv6_1" class="form-control" />
<label for="ipv6_2" class="form-label">Address 2</label>
<input type="text" id="ipv6_2" class="form-control" />
<label for="ipv6_3" class="form-label">Address 3</label>
<input type="text" id="ip64_3" class="form-control" />
</div>
</div>
<div class="row">
<div class="col" align="center">
<a href="#" class="btn btn-success"><i class='fa fa-plus'></i> Add Record</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
function start() {
colorReloadButton();
updateHostCounts();
// Get the ? search params
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params.ip != null) {
if (params.ip.includes(":")) {
$("#ipv6_1").val(params.ip + "/128");
} else {
$("#ipv4_1").val(params.ip + "/32");
}
}
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,168 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row">
<div class="col-sm-12">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-users"></i> Shaped Devices</h5>
<div class="row">
<div class="col">
<input id="search" class="form-control" placeholder="Search" style="min-width: 150px">
</div>
<div class="col">
<a href="#" class="btn btn-primary" id="btnSearch"><i class='fa fa-search'></i></a>
</div>
<div class="col">
<!--<a href="/shaped-add" class="btn btn-success"><i class='fa fa-plus'></i> Add</a>-->
</div>
</div>
<table class="table table-striped">
<thead>
<th>Circuit</th>
<th>Device</th>
<th>Plan</th>
<th>IPs</th>
<th><i class="fa fa-gear"></i></th>
</thead>
<tbody id="shapedList"></tbody>
</table>
<p>
Go to page: <span id="shapedPaginator"></span><br />
Total Shaped Devices: <span id="shapedTotal"></span>
</p>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
function fillDeviceTable(devices) {
let html = "";
for (let i=0; i<devices.length; i++) {
html += "<tr>";
html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(devices[i].circuit_id) + "'>" + devices[i].circuit_id + ": " +redactText(devices[i].circuit_name) + "</a></td>";
html += "<td class='redact'>" + devices[i].device_id + ": " + redactText(devices[i].device_name) + "</td>";
html += "<td>" + devices[i].download_max_mbps + "/" + devices[i].upload_max_mbps + "</td>";
html += "<td style='font-size: 8pt' class='redact'>";
for (let j=0; j<devices[i].ipv4.length; j++) {
html += devices[i].ipv4[j][0] + "/" + devices[i].ipv4[j][1] + "<br />";
}
for (let j=0; j<devices[i].ipv6.length; j++) {
html += devices[i].ipv6[j][0] + "/" + devices[i].ipv6[j][1] + "<br />";
}
html += "</td>";
html += "<td><a class='btn btn-primary btn-sm' href='#'><i class='fa fa-pencil'></i></a>";
html +=" <a href='#' class='btn btn-danger btn-sm'><i class='fa fa-trash'></i></a></td>";
html += "</tr>";
}
$("#shapedList").html(html);
}
function paginator(page) {
$.get("/api/shaped_devices_range/" + page * 25 + "/" + (page+1)*25, (devices) => {
fillDeviceTable(devices);
});
}
function doSearch() {
let term = $("#search").val();
if (term == "") {
paginator(0);
} else {
// /api/shaped_devices_search/<term>
let safe_term = encodeURIComponent(term);
$.get("/api/shaped_devices_search/" + safe_term, (devices) => {
fillDeviceTable(devices);
})
}
}
function start() {
setTitle();
colorReloadButton();
updateHostCounts();
$.get("/api/shaped_devices_count", (count) => {
let n_pages = count / 25;
$("#shapedTotal").text(count);
let paginator = "";
for (let i=0; i<n_pages; i++) {
paginator += "<a href='#' onclick='paginator(" + i + ")'>" + (i+1) + "</a> ";
}
$("#shapedPaginator").html(paginator);
});
$.get("/api/shaped_devices_range/0/25", (devices) => {
fillDeviceTable(devices);
});
$("#btnSearch").on('click', () => {
doSearch();
});
$("#search").on('keyup', (k) => {
if (k.originalEvent.keyCode == 13) doSearch();
});
}
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,157 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script>
</head>
<body style="background: black; font-family: Arial, Helvetica, sans-serif; height: 100%; margin: 0;">
<p style="font-size: 20pt; text-align: center; color: #dddddd" id="heading"></p>
<p style="font-size: 12pt; text-align: center; color: #dddddd">
<a href="#" onclick="init()">Refresh</a>
</p>
<div id="chart" style="width:100vw; height: 100bh; background: black;">
</div>
<script>
var option;
var routes = [];
function lerpColor(color1, color2, weight) {
var r = Math.round(color1[0] + (color2[0] - color1[0]) * weight);
var g = Math.round(color1[1] + (color2[1] - color1[1]) * weight);
var b = Math.round(color1[2] + (color2[2] - color1[2]) * weight);
return `rgb(${r}, ${g}, ${b})`;
}
function getColorForWeight(weight) {
// Define our colors as [R, G, B]
const green = [0, 128, 0];
const orange = [255, 165, 0];
const red = [255, 0, 0];
if (weight <= 0.5) {
// Scale weight to be from 0 to 1 for the green to orange transition
const adjustedWeight = weight * 2;
return lerpColor(green, orange, adjustedWeight);
} else {
// Scale weight to be from 0 to 1 for the orange to red transition
const adjustedWeight = (weight - 0.5) * 2;
return lerpColor(orange, red, adjustedWeight);
}
}
function init() {
$.get("/api/flows/lat_lon", (worldData) => {
let condensed = {};
let totalBytes = 0;
for (let i = 0; i < worldData.length; i++) {
let label = worldData[i][2];
if (label in condensed) {
condensed[label][3] += worldData[i][3]; // Bytes
totalBytes += worldData[i][3];
if (worldData[i][4] != 0) {
condensed[label][4].push(worldData[i][4]); // RTT
}
} else {
condensed[label] = worldData[i];
worldData[i][4] = [worldData[i][4]];
totalBytes += worldData[i][3];
}
}
let entries = [];
for (const [key, value] of Object.entries(condensed)) {
value[3] = value[3] / totalBytes;
entries.push(value);
}
$("#heading").text("World Data. Now tracking " + worldData.length + " flows and " + entries.length + " locations.");
var data = [{
type: 'scattergeo',
//locationmode: 'world',
lat: [],
lon: [],
hoverinfo: 'text',
text: [],
marker: {
size: [],
color: [],
line: {
color: [],
width: 2
},
}
}];
for (let i = 0; i < entries.length; i++) {
var flow = entries[i];
if (flow[4].length != 0) {
var lat = flow[0];
var lon = flow[1];
var text = flow[2];
var bytes = flow[3] * 20;
if (bytes < 5) bytes = 5;
let middle = flow[4].length -1;
var rtt = flow[4][middle] / 1000000;
rtt = rtt / 200;
if (rtt > 1) rtt = 1;
var color = getColorForWeight(rtt);
data[0].lat.push(lat);
data[0].lon.push(lon);
data[0].text.push(text);
data[0].marker.size.push(bytes);
data[0].marker.line.color.push(color);
data[0].marker.color.push(color);
}
}
var layout = {
autosize: true,
margin: {
l: 0,
r: 0,
b: 0,
t: 0,
pad: 0
},
paper_bgcolor: 'black',
geo: {
scope: 'world',
projection: {
type: 'natural earth'
},
showland: true,
showocean: true,
showlakes: true,
showrivers: true,
showcountries: true,
landcolor: 'rgb(217, 217, 217)',
subunitwidth: 1,
countrywidth: 1,
subunitcolor: 'rgb(255,255,255)',
countrycolor: 'rgb(255,255,255)',
framecolor: 'black',
bgcolor: 'black',
},
};
Plotly.newPlot('chart', data, layout, { responsive: true, displayModeBar: false });
});
}
$(document).ready(function () {
init()
});
</script>
</html>

View File

@ -1,293 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25"
height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span
id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span
id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item" id="statsLink"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload
LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row mbot8 row220">
<!-- 5 minutes of throughput -->
<!--
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-hourglass"></i> Last 5 Minutes</h5>
<div id="tpGraph" class="graph98 graph150"></div>
</div>
</div>
</div>
-->
<!-- RTT Histogram -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-bar-chart"></i> TCP Round-Trip Time Histogram</h5>
<div id="rttHistogram" class="graph98 graph150"></div>
</div>
</div>
</div>
<!-- Info -->
<div class="col-sm-4">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-tree"></i> <span id="nodeName"
style="font-weight: bold;" class='redact'></span></h5>
<strong>DL Limit</strong>: <span id="nodeDL"></span><br />
<strong>UL Limit</strong>: <span id="nodeUL"></span><br />
<div id="breadcrumbs"></div>
</div>
</div>
</div>
</div>
<div class="row" style="margin-top: 4px;">
<!-- List of network circuits -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-tree"></i> Child Nodes</h5>
<div id="treeList"></div>
</div>
</div>
</div>
<!-- List of client circuits -->
<div class="col-sm-6">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-users"></i> Attached Clients</h5>
<div id="clientList"></div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
let node = 0;
let buffers = new MultiRingBuffer(300);
let rtt_histo = new RttHistogram();
function bgColor(traffic, limit) {
if (limit == 0) {
return "#ddffdd";
}
let usage = (traffic * 8) / (limit * 1000000);
if (usage < 0.25) { return "#ddffdd" }
else if (usage < 0.5) { return "#aaffaa" }
else if (usage < 0.75) { return "#ffa500" }
else { return "#ffdddd" }
}
function getClients(rootName) {
msgPackGet("/api/tree_clients/" + encodeURI(rootName), (data) => {
let tbl = "<table class='table table-striped'>";
tbl += "<thead><th>Circuit</th><th>Limit</th><th>⬇️ DL</th><th>⬆️ UL</th></thead>";
for (let i = 0; i < data.length; ++i) {
let nodeDL = scaleNumber(data[i][Circuit.limit][0] * 1000000);
let nodeUL = scaleNumber(data[i][Circuit.limit][1] * 1000000);
if (nodeDL == "0") nodeDL = "Unlimited";
if (nodeUL == "0") nodeUL = "Unlimited";
tbl += "<tr>";
let displayName = data[i][Circuit.name];
if (displayName.length > 30) displayName = displayName.substring(0, 30) + "...";
tbl += "<td class='redact'><a href='/circuit_queue?id=" + encodeURI(data[i][Circuit.id]) + "'>" + redactText(displayName) + "</a></td>";
tbl += "<td>" + nodeDL + " / " + nodeUL + "</td>";
let upbg = bgColor(data[i][Circuit.traffic][1], data[i][Circuit.limit][1]);
let dnbg = bgColor(data[i][Circuit.traffic][0], data[0][Circuit.limit][1]);
tbl += "<td style='background-color: " + dnbg + "'>" + scaleNumber(data[i][Circuit.traffic][0] * 8) + "</td>";
tbl += "<td style='background-color: " + upbg + "'>" + scaleNumber(data[i][Circuit.traffic][1] * 8) + "</td>";
buffers.push(nodeName, data[i][Circuit.traffic][0] * 8, data[i][Circuit.traffic][1] * 8);
}
tbl += "</table>";
$("#clientList").html(tbl);
});
}
let filled_root = false;
function getTree() {
msgPackGet("/api/network_tree/" + node, (data) => {
rtt_histo.clear();
//console.log(data);
// Setup "this node"
let rootName = data[0][1][NetTrans.name];
if (!filled_root) {
$("#nodeName").text(redactText(rootName));
let nodeDL = scaleNumber(data[0][1][NetTrans.max_throughput][0] * 1000000);
let nodeUL = scaleNumber(data[0][1][NetTrans.max_throughput][1] * 1000000);
if (nodeDL == "0") nodeDL = "Unlimited";
if (nodeUL == "0") nodeUL = "Unlimited";
$("#nodeDL").text(nodeDL);
$("#nodeUL").text(nodeUL);
$.ajax({
type: "POST",
url: "/api/node_names",
data: JSON.stringify(data[0][1][NetTrans.parents]),
success: (nodeNames) => {
let breadcrumbs = "<nav aria-label='breadcrumb'>";
breadcrumbs += "<ol class='breadcrumb'>";
for (let i=0; i<data[0][1][NetTrans.parents].length; ++i) {
let bcid = data[0][1][NetTrans.parents][i];
if (bcid != node) {
let n = nodeNames.find(e => e[0] == data[0][1][NetTrans.parents][i])[1];
breadcrumbs += "<li class='breadcrumb-item redact'>";
breadcrumbs += "<a href='/tree?parent=" + data[0][1][NetTrans.parents][i] + "'>";
breadcrumbs += redactText(n);
breadcrumbs += "</a></li>";
}
}
breadcrumbs += "<li class='breadcrumb-item active redact' aria-current='page'>";
breadcrumbs += redactText(rootName);
breadcrumbs += "</li>";
breadcrumbs += "</ol>";
breadcrumbs += "</nav>";
$("#breadcrumbs").html(breadcrumbs);
}
});
filled_root = true;
}
getClients(rootName);
// Throughput graph
buffers.push(rootName, data[0][1][NetTrans.current_throughput][0] * 8, data[0][1][NetTrans.current_throughput][1] * 8);
// Build the table & update node buffers
let tbl = "<table class='table table-striped'>";
tbl += "<thead><th>Site</th><th>Limit</th><th>⬇️ DL</th><th>⬆️ UL</th><th>RTT Latency</th></thead>";
for (let i = 1; i < data.length; ++i) {
let nodeName = data[i][1][NetTrans.name];
buffers.push(nodeName, data[i][1][NetTrans.current_throughput][0] * 8, data[i][1][NetTrans.current_throughput][1] * 8);
tbl += "<tr>";
tbl += "<td class='redact'><a href='/tree?parent=" + encodeURI(data[i][0]) + "'>" + redactText(nodeName) + "</a></td>";
if (data[i][1][NetTrans.max_throughput][0] == 0 && data[i][1][NetTrans.max_throughput][1] == 0) {
tbl += "<td>No Limit</td>";
} else {
let down = scaleNumber(data[i][1][NetTrans.max_throughput][0] * 1000000);
let up = scaleNumber(data[i][1][NetTrans.max_throughput][1] * 1000000);
tbl += "<td>" + down + " / " + up + "</td>";
}
let down = scaleNumber(data[i][1][NetTrans.current_throughput][0] * 8);
let up = scaleNumber(data[i][1][NetTrans.current_throughput][1] * 8);
let dbg = bgColor(data[i][1][NetTrans.current_throughput][0], data[i][1][NetTrans.max_throughput][0]);
let ubg = bgColor(data[i][1][NetTrans.current_throughput][0], data[i][1][NetTrans.max_throughput][0]);
tbl += "<td style='background-color: " + dbg + "'>" + down + "</td>";
tbl += "<td style='background-color: " + ubg + "'>" + up + "</td>";
let rtt = "-";
if (data[i][1][NetTrans.rtts].length > 0) {
let sum = 0;
for (let j = 0; j < data[i][1][NetTrans.rtts].length; ++j) {
sum += data[i][1][NetTrans.rtts][j];
}
sum /= data[i][1][NetTrans.rtts].length;
rtt = sum.toFixed(2) + " ms";
rtt_histo.push(sum);
}
tbl += "<td>" + rtt + "</td>";
tbl += "</tr>";
}
tbl += "</table>";
$("#treeList").html(tbl);
// Build the stacked chart
//buffers.plotStackedBars("tpGraph", rootName);
// Build the RTT histo
rtt_histo.plot("rttHistogram");
});
if (isRedacted()) {
//console.log("Redacting");
//css_getclass(".redact").style.filter = "blur(4px)";
css_getclass(".redact").style.fontFamily = "klingon";
}
setTimeout(getTree, 1000);
}
function start() {
setTitle();
for (let i = 0; i < 20; ++i) rtt_histo.push(0);
colorReloadButton();
updateHostCounts();
getTree();
}
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
node = params.parent;
$(document).ready(start);
</script>
</body>
</html>

View File

@ -1,132 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/vendor/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/vendor/solid.min.css">
<link rel="stylesheet" href="/lqos.css">
<link rel="icon" href="/favicon.png">
<title>LibreQoS - Local Node Manager</title>
<script src="/lqos.js"></script>
<script src="/vendor/plotly-2.16.1.min.js"></script>
<script src="/vendor/jquery.min.js"></script><script src="/vendor/msgpack.min.js"></script>
<script defer src="/vendor/bootstrap.bundle.min.js"></script>
</head>
<body class="bg-secondary">
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/"><img src="/vendor/tinylogo.svg" alt="LibreQoS SVG Logo" width="25" height="25" />&nbsp;LibreQoS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/tree?parent=0"><i class="fa fa-tree"></i> Tree</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/shaped"><i class="fa fa-users"></i> Shaped Devices <span id="shapedCount" class="badge badge-pill badge-success green-badge">?</span></a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/unknown"><i class="fa fa-address-card"></i> Unknown IPs <span id="unshapedCount" class="badge badge-warning orange-badge">?</span></a>
</li>
</ul>
</div>
<ul class="navbar-nav ms-auto">
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item ms-auto">
<a class="nav-link" href="/help"><i class="fa fa-question-circle"></i> Help</a>
</li>
<li>
<a class="nav-link btn btn-small black-txt" href="#" id="btnReload"><i class="fa fa-refresh"></i> Reload LibreQoS</a>
</li>
</ul>
</div>
</nav>
<div id="container" class="pad4">
<div class="row">
<div class="col-sm-12">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-address-card"></i> Unmapped IP Addresses (Most recently seen first)</h5>
<a id="btnDownloadCsv" class="btn btn-info"><i class="fa fa-download"></i> Download Text File of Unknown IP addresses.</a>
<table class="table table-striped">
<thead>
<th>IP</th>
<th>Total Bandwidth</th>
<th>Total Packets</th>
<th><i class='fa fa-gear'></i></th>
</thead>
<tbody id="unknownList"></tbody>
</table>
<p>
Go to page: <span id="unknownPaginator"></span><br />
Total Shaped Devices: <span id="unknownTotal"></span>
</p>
</div>
</div>
</div>
</div>
</div>
<footer>&copy; 2022-2023, LibreQoE LLC</footer>
<script>
function fillDeviceTable(devices) {
let html = "";
for (let i=0; i<devices.length; i++) {
html += "<tr>";
html += "<td>" + devices[i].ip_address + "</td>";
html += "<td>" + scaleNumber(devices[i].bits_per_second[0]) + " / " + scaleNumber(devices[i].bits_per_second[1]) + "</td>";
html += "<td>" + scaleNumber(devices[i].packets_per_second[0]) + " / " + scaleNumber(devices[i].packets_per_second[1]) + "</td>";
//html += "<td><a class='btn btn-small btn-success' href='/shaped-add?ip=" + devices[i].ip_address + "'><i class='fa fa-plus'></i></a></td>";
html += "<td></td>";
html += "</tr>";
}
$("#unknownList").html(html);
}
function paginator(page) {
$.get("/api/unknown_devices_range/" + page * 25 + "/" + (page+1)*25, (devices) => {
fillDeviceTable(devices);
});
}
function start() {
colorReloadButton();
updateHostCounts();
$.get("/api/unknown_devices_count", (count) => {
let n_pages = count / 25;
$("#unknownTotal").text(count);
let paginator = "";
for (let i=0; i<n_pages; i++) {
paginator += "<a href='#' onclick='paginator(" + i + ")'>" + (i+1) + "</a> ";
}
$("#unknownPaginator").html(paginator);
});
$.get("/api/unknown_devices_range/0/25", (devices) => {
console.log(devices);
fillDeviceTable(devices);
});
$("#btnDownloadCsv").on('click', () => {
window.location.href = "/api/unknown_devices_csv";
});
}
$(document).ready(start);
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,14 +9,14 @@ name = "lqos_python"
crate-type = ["cdylib"]
[dependencies]
pyo3 = "0"
pyo3 = { workspace = true }
lqos_bus = { path = "../lqos_bus" }
lqos_utils = { path = "../lqos_utils" }
lqos_config = { path = "../lqos_config" }
tokio = { version = "1", features = [ "full" ] }
anyhow = "1"
sysinfo = "0"
nix = "0"
reqwest = { version = "0.11.24", features = ["blocking"] }
serde = { version = "1.0.196", features = ["derive"] }
tokio = { workspace = true }
anyhow = { workspace = true }
sysinfo = { workspace = true }
nix = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
chrono = "0.4.33"

View File

@ -139,7 +139,7 @@ fn recurse_weights(
node_index: usize,
) -> Result<i64> {
let mut weight = 0;
let n = &network.nodes[node_index];
let n = &network.get_nodes_when_ready()[node_index];
//println!(" Tower: {}", n.name);
device_list
@ -152,7 +152,7 @@ fn recurse_weights(
});
//println!(" Weight: {}", weight);
for (i, n) in network.nodes
for (i, _n) in network.get_nodes_when_ready()
.iter()
.enumerate()
.filter(|(_i, n)| n.immediate_parent == Some(node_index))
@ -177,13 +177,13 @@ pub(crate) fn calculate_tree_weights() -> Result<Vec<NetworkNodeWeight>> {
let device_list = ConfigShapedDevices::load()?.devices;
let device_weights = get_weights_rust()?;
let network = lqos_config::NetworkJson::load()?;
let root_index = network.nodes.iter().position(|n| n.immediate_parent.is_none()).unwrap();
let root_index = network.get_nodes_when_ready().iter().position(|n| n.immediate_parent.is_none()).unwrap();
let mut result = Vec::new();
//println!("Root index is: {}", root_index);
// Find all network nodes one off the top
network
.nodes
.get_nodes_when_ready()
.iter()
.enumerate()
.filter(|(_,n)| n.immediate_parent.is_some() && n.immediate_parent.unwrap() == root_index)

View File

@ -5,18 +5,18 @@ edition = "2021"
license = "GPL-2.0-only"
[dependencies]
thiserror = "1"
serde = "1"
serde_json = "1"
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
lqos_bus = { path = "../lqos_bus" }
lqos_config = { path = "../lqos_config" }
lqos_sys = { path = "../lqos_sys" }
lqos_utils = { path = "../lqos_utils" }
log = "0"
log = { workspace = true }
log-once = "0.4.0"
tokio = { version = "1", features = [ "full", "parking_lot" ] }
once_cell = "1"
dashmap = "5"
tokio = { workspace = true }
once_cell = { workspace = true}
dashmap = { workspace = true }
[dev-dependencies]
criterion = { version = "0", features = [ "html_reports"] }

View File

@ -23,3 +23,4 @@ pub use queue_structure::spawn_queue_structure_monitor;
pub use queue_types::deserialize_tc_tree; // Exported for the benchmarker
pub use tracking::spawn_queue_monitor;
pub use tracking::{add_watched_queue, still_watching};
pub use tracking::{ALL_QUEUE_SUMMARY, TOTAL_QUEUE_STATS};

View File

@ -5,7 +5,7 @@ use log::error;
pub use queing_structure_json_monitor::spawn_queue_structure_monitor;
pub(crate) use queing_structure_json_monitor::QUEUE_STRUCTURE;
use queue_network::QueueNetwork;
use queue_node::QueueNode;
pub(crate) use queue_node::QueueNode;
use thiserror::Error;
pub(crate) fn read_queueing_structure(

View File

@ -8,6 +8,7 @@ use lqos_utils::file_watcher::FileWatcher;
use once_cell::sync::Lazy;
use thiserror::Error;
use tokio::task::spawn_blocking;
use crate::tracking::ALL_QUEUE_SUMMARY;
pub(crate) static QUEUE_STRUCTURE: Lazy<RwLock<QueueStructure>> =
Lazy::new(|| RwLock::new(QueueStructure::new()));
@ -27,6 +28,7 @@ impl QueueStructure {
}
fn update(&mut self) {
ALL_QUEUE_SUMMARY.clear();
if let Ok(queues) = read_queueing_structure() {
self.maybe_queues = Some(queues);
} else {
@ -49,7 +51,7 @@ fn update_queue_structure() {
}
/// Fires up a Linux file system watcher than notifies
/// when `ShapedDevices.csv` changes, and triggers a reload.
/// when `queuingStructure.json` changes, and triggers a reload.
fn watch_for_queueing_structure_changing() -> Result<(), QueueWatcherError> {
// Obtain the path to watch
let watch_path = QueueNetwork::path();

View File

@ -0,0 +1,142 @@
use std::collections::HashMap;
use std::sync::Mutex;
use once_cell::sync::Lazy;
use lqos_utils::units::{AtomicDownUp, DownUpOrder};
use crate::tracking::TrackedQueue;
/// Holds all of the CAKE queue summaries being tracked by the system.
pub static ALL_QUEUE_SUMMARY: Lazy<AllQueueData> = Lazy::new(|| AllQueueData::new());
/// Tracks the total number of drops and marks across all queues.
pub static TOTAL_QUEUE_STATS: TotalQueueStats = TotalQueueStats::new();
pub struct TotalQueueStats {
pub drops: AtomicDownUp,
pub marks: AtomicDownUp,
}
impl TotalQueueStats {
pub const fn new() -> Self {
Self {
drops: AtomicDownUp::zeroed(),
marks: AtomicDownUp::zeroed(),
}
}
}
#[derive(Debug)]
pub struct QueueData {
pub drops: DownUpOrder<u64>,
pub marks: DownUpOrder<u64>,
pub prev_drops: Option<DownUpOrder<u64>>,
pub prev_marks: Option<DownUpOrder<u64>>,
}
fn zero_total_queue_stats() {
TOTAL_QUEUE_STATS.drops.set_to_zero();
TOTAL_QUEUE_STATS.marks.set_to_zero();
}
#[derive(Debug)]
pub struct AllQueueData {
data: Mutex<HashMap<String, QueueData>>,
}
impl AllQueueData {
pub fn new() -> Self {
Self { data: Mutex::new(HashMap::new()) }
}
pub fn clear(&self) {
let mut lock = self.data.lock().unwrap();
lock.clear();
}
pub fn ingest_batch(&self, download: Vec<TrackedQueue>, upload: Vec<TrackedQueue>) {
let mut lock = self.data.lock().unwrap();
// Roll through moving current to previous
for (_, q) in lock.iter_mut() {
q.prev_drops = Some(q.drops);
q.prev_marks = Some(q.marks);
q.drops = DownUpOrder::zeroed();
q.marks = DownUpOrder::zeroed();
}
// Make download markings
for dl in download.into_iter() {
if let Some(q) = lock.get_mut(&dl.circuit_id) {
// We need to update it
q.drops.down = dl.drops;
q.marks.down = dl.marks;
} else {
// We need to add it
let mut new_record = QueueData {
drops: Default::default(),
marks: Default::default(),
prev_drops: None,
prev_marks: None,
};
new_record.drops.down = dl.drops;
new_record.marks.down = dl.marks;
lock.insert(dl.circuit_id.clone(), new_record);
}
}
// Make upload markings
for ul in upload.into_iter() {
if let Some(q) = lock.get_mut(&ul.circuit_id) {
// We need to update it
q.drops.up = ul.drops;
q.marks.up = ul.marks;
} else {
// We need to add it
let mut new_record = QueueData {
drops: Default::default(),
marks: Default::default(),
prev_drops: Default::default(),
prev_marks: Default::default(),
};
new_record.drops.up = ul.drops;
new_record.marks.up = ul.marks;
lock.insert(ul.circuit_id.clone(), new_record);
}
}
}
pub fn iterate_queues(&self, mut f: impl FnMut(&str, &DownUpOrder<u64>, &DownUpOrder<u64>)) {
let lock = self.data.lock().unwrap();
for (circuit_id, q) in lock.iter() {
if let Some(prev_drops) = q.prev_drops {
if let Some(prev_marks) = q.prev_marks {
if q.drops > prev_drops || q.marks > prev_marks {
let drops = q.drops.checked_sub_or_zero(prev_drops);
let marks = q.marks.checked_sub_or_zero(prev_marks);
f(circuit_id, &drops, &marks);
}
}
}
}
}
pub fn calculate_total_queue_stats(&self) {
zero_total_queue_stats();
let lock = self.data.lock().unwrap();
let mut drops = DownUpOrder::zeroed();
let mut marks = DownUpOrder::zeroed();
lock
.iter()
.filter(|(_, q)| q.prev_drops.is_some() && q.prev_marks.is_some())
.for_each(|(_, q)| {
drops += q.drops.checked_sub_or_zero(q.prev_drops.unwrap());
marks += q.marks.checked_sub_or_zero(q.prev_marks.unwrap());
});
TOTAL_QUEUE_STATS.drops.set_down(drops.down);
TOTAL_QUEUE_STATS.drops.set_up(drops.up);
TOTAL_QUEUE_STATS.marks.set_down(marks.down);
TOTAL_QUEUE_STATS.marks.set_up(marks.up);
}
}

View File

@ -1,3 +1,4 @@
use std::time::Instant;
use crate::{
circuit_to_queue::CIRCUIT_TO_QUEUE, interval::QUEUE_MONITOR_INTERVAL,
queue_store::QueueStore, tracking::reader::read_named_queue_from_interface,
@ -6,9 +7,15 @@ use log::info;
use lqos_utils::fdtimer::periodic;
mod reader;
mod watched_queues;
mod all_queue_data;
pub use all_queue_data::*;
use self::watched_queues::expire_watched_queues;
use watched_queues::WATCHED_QUEUES;
pub use watched_queues::{add_watched_queue, still_watching};
use crate::queue_structure::{QUEUE_STRUCTURE, QueueNode};
use crate::queue_types::QueueType;
use crate::tracking::reader::read_all_queues_from_interface;
fn track_queues() {
if WATCHED_QUEUES.is_empty() {
@ -75,6 +82,106 @@ fn track_queues() {
expire_watched_queues();
}
/// Holds the CAKE marks/drops for a given queue/circuit.
pub struct TrackedQueue {
circuit_id: String,
drops: u64,
marks: u64,
}
fn connect_queues_to_circuit(structure: &[QueueNode], queues: &[QueueType]) -> Vec<TrackedQueue> {
queues
.iter()
.filter_map(|q| {
if let QueueType::Cake(cake) = q {
let (major, minor) = cake.parent.get_major_minor();
if let Some (s) = structure.iter().find(|s| s.class_major == major as u32 && s.class_minor == minor as u32) {
if let Some(circuit_id) = &s.circuit_id {
let marks: u32 = cake.tins.iter().map(|tin| tin.ecn_marks).sum();
if cake.drops > 0 || marks > 0 {
return Some(TrackedQueue {
circuit_id: circuit_id.clone(),
drops: cake.drops as u64,
marks: marks as u64,
})
}
}
}
}
None
})
.collect()
}
fn connect_queues_to_circuit_up(structure: &[QueueNode], queues: &[QueueType]) -> Vec<TrackedQueue> {
queues
.iter()
.filter_map(|q| {
if let QueueType::Cake(cake) = q {
let (major, minor) = cake.parent.get_major_minor();
if let Some (s) = structure.iter().find(|s| s.up_class_major == major as u32 && s.class_minor == minor as u32) {
if let Some(circuit_id) = &s.circuit_id {
let marks: u32 = cake.tins.iter().map(|tin| tin.ecn_marks).sum();
if cake.drops > 0 || marks > 0 {
return Some(TrackedQueue {
circuit_id: circuit_id.clone(),
drops: cake.drops as u64,
marks: marks as u64,
})
}
}
}
}
None
})
.collect()
}
fn all_queue_reader() {
let start = Instant::now();
let structure = QUEUE_STRUCTURE.read().unwrap();
if let Some(structure) = &structure.maybe_queues {
if let Ok(config) = lqos_config::load_config() {
// Get all the queues
let (download, upload) = if config.on_a_stick_mode() {
let all_queues = read_all_queues_from_interface(&config.internet_interface());
let (download, upload) = if let Ok(q) = all_queues {
let download = connect_queues_to_circuit(&structure, &q);
let upload = connect_queues_to_circuit_up(&structure, &q);
(download, upload)
} else {
(Vec::new(), Vec::new())
};
(download, upload)
} else {
let all_queues_down = read_all_queues_from_interface(&config.internet_interface());
let all_queues_up = read_all_queues_from_interface(&config.isp_interface());
let download = if let Ok(q) = all_queues_down {
connect_queues_to_circuit(&structure, &q)
} else {
Vec::new()
};
let upload = if let Ok(q) = all_queues_up {
connect_queues_to_circuit(&structure, &q)
} else {
Vec::new()
};
(download, upload)
};
//println!("{}", download.len() + upload.len());
ALL_QUEUE_SUMMARY.ingest_batch(download, upload);
} else {
log::warn!("(TC monitor) Unable to read configuration");
}
} else {
log::warn!("(TC monitor) Not reading queues due to structure not yet ready");
}
let elapsed = start.elapsed();
log::debug!("(TC monitor) Completed in {:.5} seconds", elapsed.as_secs_f32());
}
/// Spawns a thread that periodically reads the queue statistics from
/// the Linux `tc` shaper, and stores them in a `QueueStore` for later
/// retrieval.
@ -96,4 +203,11 @@ pub fn spawn_queue_monitor() {
track_queues();
});
});
// Set up a 2nd thread to periodically gather ALL the queue stats
std::thread::spawn(|| {
periodic(2000, "All Queues", &mut || {
all_queue_reader();
})
});
}

View File

@ -1,11 +1,46 @@
use crate::{deserialize_tc_tree, queue_types::QueueType};
use log::error;
use log::{error, info};
use lqos_bus::TcHandle;
use std::process::Command;
use thiserror::Error;
const TC: &str = "/sbin/tc";
pub fn read_all_queues_from_interface(
interface: &str
) -> Result<Vec<QueueType>, QueueReaderError> {
let command_output = Command::new(TC)
.args([
"-s",
"-j",
"qdisc",
"show",
"dev",
interface,
])
.output()
.map_err(|e| {
info!("Failed to poll TC for queues: {interface}");
info!("{:?}", e);
QueueReaderError::CommandError
})?;
let raw_json = String::from_utf8(command_output.stdout)
.map_err(|e| {
info!("Failed to convert byte stream to UTF-8 string");
info!("{:?}", e);
QueueReaderError::Utf8Error
})?;
let result = deserialize_tc_tree(&raw_json)
.map_err(|e| {
info!("Failed to deserialize TC tree result.");
info!("{:?}", e);
QueueReaderError::Deserialization
})?;
Ok(result)
}
pub fn read_named_queue_from_interface(
interface: &str,
tc_handle: TcHandle,

View File

@ -5,8 +5,7 @@ edition = "2021"
license = "GPL-2.0-only"
[dependencies]
colored = "2"
default-net = "0" # For obtaining an easy-to-use NIC list
uuid = { version = "1", features = ["v4", "fast-rng" ] }
colored = { workspace = true }
default-net = { workspace = true }
lqos_config = { path = "../lqos_config" }
toml = "0.8.8"
toml = { workspace = true }

View File

@ -1,6 +1,5 @@
use colored::Colorize;
use default_net::{get_interfaces, interface::InterfaceType, Interface};
use uuid::Uuid;
use std::{fs, path::Path, process::Command};
fn get_available_interfaces() -> Vec<Interface> {

View File

@ -4,14 +4,13 @@ version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
clap = { version = "4.5.7", features = ["derive"] }
colored = "2.1.0"
anyhow = { workspace = true }
clap = { workspace = true }
colored = { workspace = true }
lqos_config = { path = "../lqos_config" }
serde = { version = "1.0", features = ["derive"] }
serde_cbor = "0" # For RFC8949/7409 format C binary objects
miniz_oxide = "0.7.1"
log = "0.4.21" # For compression
serde = { workspace = true }
serde_cbor = { workspace = true }
miniz_oxide = { workspace = true }
libc = "0.2.155"
nix = "0.29.0"
serde_json = "1.0.117"
nix = { workspace = true }
serde_json = { workspace = true }

View File

@ -38,7 +38,7 @@ enum Commands {
}
fn read_line() -> String {
use std::io::{stdin,stdout,Write};
use std::io::stdin;
let mut s = String::new();
stdin().read_line(&mut s).expect("Did not enter a correct string");
s.trim().to_string()

View File

@ -90,7 +90,6 @@ pub fn check_interface_status(results: &mut Vec<SanityCheck>) {
#[derive(Debug)]
struct IpLinkInterface {
pub name: String,
pub index: u32,
pub operational_state: String,
pub link_type: String,
pub master: Option<String>,
@ -106,14 +105,12 @@ fn get_interfaces_from_ip_link() -> anyhow::Result<Vec<IpLinkInterface>> {
let mut interfaces = Vec::new();
for interface in output_json.as_array().unwrap() {
let name = interface["ifname"].as_str().unwrap().to_string();
let index = interface["ifindex"].as_u64().unwrap() as u32;
let operstate = interface["operstate"].as_str().unwrap().to_string();
let link_type = interface["link_type"].as_str().unwrap().to_string();
let master = interface["master"].as_str().map(|s| s.to_string());
interfaces.push(IpLinkInterface {
name,
index,
operational_state: operstate,
link_type,
master,

View File

@ -28,7 +28,7 @@ pub fn can_we_load_net_json(results: &mut Vec<SanityCheck>) {
if path.exists() {
if let Ok(str) = std::fs::read_to_string(path) {
match serde_json::from_str::<Value>(&str) {
Ok(json) => {
Ok(_json) => {
results.push(SanityCheck{
name: "network.json is parseable JSON".to_string(),
success: true,
@ -50,7 +50,7 @@ pub fn can_we_load_net_json(results: &mut Vec<SanityCheck>) {
pub fn can_we_parse_net_json(results: &mut Vec<SanityCheck>) {
match lqos_config::NetworkJson::load() {
Ok(json) => {
Ok(_json) => {
results.push(SanityCheck{
name: "network.json is valid JSON".to_string(),
success: true,

View File

@ -42,7 +42,7 @@ pub fn can_we_read_shaped_devices(results: &mut Vec<SanityCheck>) {
pub fn parent_check(results: &mut Vec<SanityCheck>) {
if let Ok(net_json) = lqos_config::NetworkJson::load() {
if net_json.nodes.len() < 2 {
if net_json.get_nodes_when_ready().len() < 2 {
results.push(SanityCheck{
name: "Flat Network - Skipping Parent Check".to_string(),
success: true,
@ -53,7 +53,7 @@ pub fn parent_check(results: &mut Vec<SanityCheck>) {
if let Ok(shaped_devices) = lqos_config::ConfigShapedDevices::load() {
for sd in shaped_devices.devices.iter() {
if !net_json.nodes.iter().any(|n| n.name == sd.parent_node) {
if !net_json.get_nodes_when_ready().iter().any(|n| n.name == sd.parent_node) {
results.push(SanityCheck{
name: "Shaped Device Invalid Parent".to_string(),
success: false,

View File

@ -5,18 +5,16 @@ edition = "2021"
license = "GPL-2.0-only"
[dependencies]
nix = "0"
nix = { workspace = true }
libbpf-sys = "1"
anyhow = "1"
byteorder = "1.4"
anyhow = { workspace = true }
lqos_bus = { path = "../lqos_bus" }
lqos_config = { path = "../lqos_config" }
log = "0"
log = { workspace = true }
lqos_utils = { path = "../lqos_utils" }
once_cell = "1"
dashmap = "5"
thiserror = "1"
zerocopy = { version = "0.6.1", features = ["simd"] }
once_cell = { workspace = true}
thiserror = { workspace = true }
zerocopy = { workspace = true }
[build-dependencies]
bindgen = "0"

View File

@ -336,12 +336,13 @@ static __always_inline void track_flows(
) {
u_int8_t rate_index;
u_int8_t other_rate_index;
// Ensure that we get DownUp order in the lqosd map
if (direction == TO_INTERNET) {
rate_index = 0;
other_rate_index = 1;
} else {
rate_index = 1;
other_rate_index = 0;
} else {
rate_index = 0;
other_rate_index = 1;
}
// Pass to the appropriate protocol handler

View File

@ -2,6 +2,7 @@
use lqos_utils::XdpIpAddress;
use zerocopy::FromBytes;
use lqos_utils::units::DownUpOrder;
/// Representation of the eBPF `flow_key_t` type.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, FromBytes)]
@ -32,29 +33,29 @@ pub struct FlowbeeData {
/// Time (nanos) when the connection was last seen
pub last_seen: u64,
/// Bytes transmitted
pub bytes_sent: [u64; 2],
pub bytes_sent: DownUpOrder<u64>,
/// Packets transmitted
pub packets_sent: [u64; 2],
pub packets_sent: DownUpOrder<u64>,
/// Clock for the next rate estimate
pub next_count_time: [u64; 2],
pub next_count_time: DownUpOrder<u64>,
/// Clock for the previous rate estimate
pub last_count_time: [u64; 2],
pub last_count_time: DownUpOrder<u64>,
/// Bytes at the next rate estimate
pub next_count_bytes: [u64; 2],
pub next_count_bytes: DownUpOrder<u64>,
/// Rate estimate
pub rate_estimate_bps: [u32; 2],
pub rate_estimate_bps: DownUpOrder<u32>,
/// Sequence number of the last packet
pub last_sequence: [u32; 2],
pub last_sequence: DownUpOrder<u32>,
/// Acknowledgement number of the last packet
pub last_ack: [u32; 2],
pub last_ack: DownUpOrder<u32>,
/// TCP Retransmission count (also counts duplicates)
pub tcp_retransmits: [u16; 2],
pub tcp_retransmits: DownUpOrder<u16>,
/// Timestamp values
pub tsval: [u32; 2],
pub tsval: DownUpOrder<u32>,
/// Timestamp echo values
pub tsecr: [u32; 2],
pub tsecr: DownUpOrder<u32>,
/// When did the timestamp change?
pub ts_change_time: [u64; 2],
pub ts_change_time: DownUpOrder<u64>,
/// Has the connection ended?
/// 0 = Alive, 1 = FIN, 2 = RST
pub end_status: u8,

View File

@ -5,10 +5,11 @@ edition = "2021"
license = "GPL-2.0-only"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
nix = { version = "0", features = ["time"] }
log = "0"
serde = { workspace = true }
nix = { workspace = true }
log = { workspace = true }
notify = { version = "5.0.0", default-features = false } # Not using crossbeam because of Tokio
thiserror = "1"
byteorder = "1.4"
zerocopy = { version = "0.6.1", features = ["simd"] }
thiserror = { workspace = true }
byteorder = { workspace = true }
zerocopy = { workspace = true }
num-traits = { workspace = true }

View File

@ -19,6 +19,8 @@ mod string_table_enum;
/// Utilities dealing with Unix Timestamps
pub mod unix_time;
mod xdp_ip_address;
/// Helpers for units of measurement
pub mod units;
/// XDP compatible IP Address
pub use xdp_ip_address::XdpIpAddress;

View File

@ -0,0 +1,7 @@
mod atomic_down_up;
mod down_up;
mod up_down;
pub use atomic_down_up::*;
pub use down_up::*;
pub use up_down::*;

View File

@ -0,0 +1,120 @@
//! AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
//! We frequently order things down and then up in kernel maps, keeping the ordering explicit
//! helps reduce directional confusion/bugs.
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering::Relaxed;
use crate::units::DownUpOrder;
/// AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
/// It's typically used for throughput, but can be used for any pairing that needs to keep track
/// of values by direction.
///
/// Note that unlike the DownUpOrder struct, it is not intended for direct serialization, and
/// is not generic.
#[derive(Debug)]
pub struct AtomicDownUp {
down: AtomicU64,
up: AtomicU64,
}
impl AtomicDownUp {
/// Create a new `AtomicDownUp` with both values set to zero.
pub const fn zeroed() -> Self {
Self {
down: AtomicU64::new(0),
up: AtomicU64::new(0),
}
}
/// Set both down and up to zero.
pub fn set_to_zero(&self) {
self.up.store(0, Relaxed);
self.down.store(0, Relaxed);
}
/// Add a tuple of u64 values to the down and up values. The addition
/// is checked, and will not occur if it would result in an overflow.
pub fn checked_add_tuple(&self, n: (u64, u64)) {
let n0 = self.down.load(std::sync::atomic::Ordering::Relaxed);
if let Some(n) = n0.checked_add(n.0) {
self.down.store(n, std::sync::atomic::Ordering::Relaxed);
}
let n1 = self.up.load(std::sync::atomic::Ordering::Relaxed);
if let Some(n) = n1.checked_add(n.1) {
self.up.store(n, std::sync::atomic::Ordering::Relaxed);
}
}
/// Add a DownUpOrder to the down and up values. The addition
/// is checked, and will not occur if it would result in an overflow.
pub fn checked_add(&self, n: DownUpOrder<u64>) {
let n0 = self.down.load(std::sync::atomic::Ordering::Relaxed);
if let Some(n) = n0.checked_add(n.down) {
self.down.store(n, std::sync::atomic::Ordering::Relaxed);
}
let n1 = self.up.load(std::sync::atomic::Ordering::Relaxed);
if let Some(n) = n1.checked_add(n.up) {
self.up.store(n, std::sync::atomic::Ordering::Relaxed);
}
}
/// Get the down value.
pub fn get_down(&self) -> u64 {
self.down.load(Relaxed)
}
/// Get the up value.
pub fn get_up(&self) -> u64 {
self.up.load(Relaxed)
}
/// Set the down value.
pub fn set_down(&self, n: u64) {
self.down.store(n, Relaxed);
}
/// Set the up value.
pub fn set_up(&self, n: u64) {
self.up.store(n, Relaxed);
}
/// Transform the AtomicDownUp into a `DownUpOrder<u64>`.
pub fn as_down_up(&self) -> DownUpOrder<u64> {
DownUpOrder::new(
self.get_down(),
self.get_up()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atomic_down_up() {
let adu = AtomicDownUp::zeroed();
assert_eq!(adu.get_down(), 0);
assert_eq!(adu.get_up(), 0);
adu.set_down(1);
adu.set_up(2);
assert_eq!(adu.get_down(), 1);
assert_eq!(adu.get_up(), 2);
adu.checked_add(DownUpOrder::new(1, 2));
assert_eq!(adu.get_down(), 2);
assert_eq!(adu.get_up(), 4);
adu.checked_add_tuple((1, 2));
assert_eq!(adu.get_down(), 3);
assert_eq!(adu.get_up(), 6);
adu.set_to_zero();
assert_eq!(adu.get_down(), 0);
assert_eq!(adu.get_up(), 0);
}
}

View File

@ -0,0 +1,191 @@
//! AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
//! We frequently order things down and then up in kernel maps, keeping the ordering explicit
//! helps reduce directional confusion/bugs.
use std::ops::AddAssign;
use serde::{Deserialize, Serialize};
use zerocopy::FromBytes;
use crate::units::UpDownOrder;
/// Provides strong download/upload separation for
/// stored statistics to eliminate confusion. This is a generic
/// type: you can control the type stored inside.
#[repr(C)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, FromBytes, Default, Ord, PartialOrd)]
pub struct DownUpOrder<T> {
/// The down value
pub down: T,
/// The up value
pub up: T,
}
impl <T> DownUpOrder<T>
where T: std::cmp::Ord + num_traits::Zero + Copy + num_traits::CheckedSub
+ num_traits::CheckedAdd + num_traits::SaturatingSub + num_traits::SaturatingMul
+ num_traits::FromPrimitive
{
/// Create a new DownUpOrder with the given down and up values.
pub fn new(down: T, up: T) -> Self {
Self { down, up }
}
/// In the C code, it's common to refer to a "direction" byte:
///
/// * 0: down
/// * 1: up
/// * >1: error
///
/// This is a helper function to translate that byte into the
/// appropriate value.
pub fn dir(&self, direction: usize) -> T {
if direction == 0 {
self.down
} else {
self.up
}
}
/// Return a new DownUpOrder with both down and up set to zero.
pub fn zeroed() -> Self {
Self { down: T::zero(), up: T::zero() }
}
/// Check if both down and up are less than the given limit.
/// Returns `true` if they are both less than the limit, `false` otherwise.
pub fn both_less_than(&self, limit: T) -> bool {
self.down < limit && self.up < limit
}
/// Check if the sum of down and up exceeds the given limit.
pub fn sum_exceeds(&self, limit: T) -> bool {
self.down + self.up > limit
}
/// Subtract the given DownUpOrder from this one, returning a new DownUpOrder.
/// If the result would be negative, it is clamped to zero.
pub fn checked_sub_or_zero(&self, rhs: DownUpOrder<T>) -> DownUpOrder<T> {
let down = T::checked_sub(&self.down, &rhs.down).unwrap_or(T::zero());
let up = T::checked_sub(&self.up, &rhs.up).unwrap_or(T::zero());
DownUpOrder { down, up }
}
/// Add the given DownUpOrder to this one. If the result would overflow,
/// it is set to zero.
pub fn checked_add(&mut self, rhs: DownUpOrder<T>) {
self.down = self.down.checked_add(&rhs.down).unwrap_or(T::zero());
self.up = self.up.checked_add(&rhs.up).unwrap_or(T::zero());
}
/// Add the given down and up values to this DownUpOrder. If the result would overflow,
/// it is set to zero.
pub fn checked_add_direct(&mut self, down: T, up: T) {
self.down = self.down.checked_add(&down).unwrap_or(T::zero());
self.up = self.up.checked_add(&up).unwrap_or(T::zero());
}
/// Add the given tuple of down and up values to this DownUpOrder. If the result would overflow,
/// it is set to zero.
pub fn checked_add_tuple(&mut self, (down, up): (T, T)) {
self.down = self.down.checked_add(&down).unwrap_or(T::zero());
self.up = self.up.checked_add(&up).unwrap_or(T::zero());
}
/// Add the `down` and `up` values, giving a total.
pub fn sum(&self) -> T {
self.down + self.up
}
/// Multiply the `down` and `up` values by 8, giving the total number of bits, assuming
/// that the previous value was bytes.
pub fn to_bits_from_bytes(&self) -> DownUpOrder<T> {
DownUpOrder {
down: self.down.saturating_mul(&T::from_u32(8).unwrap()),
up: self.up.saturating_mul(&T::from_u32(8).unwrap()),
}
}
/// Get the `down` value.
pub fn get_down(&self) -> T {
self.down
}
/// Get the `up` value.
pub fn get_up(&self) -> T {
self.up
}
/// Set both the `down` and `up` values to zero.
pub fn set_to_zero(&mut self) {
self.down = T::zero();
self.up = T::zero();
}
}
impl <T> Into<UpDownOrder<T>> for DownUpOrder<T> {
fn into(self) -> UpDownOrder<T> {
UpDownOrder {
up: self.down,
down: self.up
}
}
}
impl <T> AddAssign for DownUpOrder<T>
where T: std::cmp::Ord + num_traits::Zero + Copy + num_traits::CheckedAdd
{
fn add_assign(&mut self, rhs: Self) {
self.down = self.down.checked_add(&rhs.down).unwrap_or(T::zero());
self.up = self.up.checked_add(&rhs.up).unwrap_or(T::zero());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverse() {
let a = UpDownOrder::new(1, 2);
let b: DownUpOrder<i32> = a.into();
assert_eq!(a.down, b.up);
}
#[test]
fn test_checked_sub() {
let a = DownUpOrder::new(1u64, 1);
let b= DownUpOrder::new(1, 1);
let c = a.checked_sub_or_zero(b);
assert_eq!(c.up, 0);
assert_eq!(c.down, 0);
let b= DownUpOrder::new(2, 2);
let c = a.checked_sub_or_zero(b);
assert_eq!(c.up, 0);
assert_eq!(c.down, 0);
}
#[test]
fn test_checked_add() {
let mut a = DownUpOrder::new(u64::MAX, u64::MAX);
let b = DownUpOrder::new(1, 1);
a.checked_add(b);
assert_eq!(a.down, 0);
assert_eq!(a.up, 0);
let mut a = DownUpOrder::new(1, 2);
a.checked_add(b);
assert_eq!(a.down, 2);
assert_eq!(a.up, 3);
}
#[test]
fn test_checked_add_direct() {
let mut a = DownUpOrder::new(u64::MAX, u64::MAX);
a.checked_add_direct(1, 1);
assert_eq!(a.down, 0);
assert_eq!(a.up, 0);
let mut a = DownUpOrder::new(1, 2);
a.checked_add_direct(1, 1);
assert_eq!(a.down, 2);
assert_eq!(a.up, 3);
}
}

View File

@ -0,0 +1,101 @@
//! AtomicDownUp is a struct that contains two atomic u64 values, one for down and one for up.
//! We frequently order things down and then up in kernel maps, keeping the ordering explicit
//! helps reduce directional confusion/bugs.
use crate::units::DownUpOrder;
/// Provides strong download/upload separation for
/// stored statistics to eliminate confusion.
#[repr(C)]
#[derive(Copy, Clone, Debug)]
pub struct UpDownOrder<T> {
/// The up value
pub up: T,
/// The down value
pub down: T,
}
impl<T> UpDownOrder<T>
where T: std::cmp::Ord + num_traits::Zero + Copy + num_traits::CheckedSub
+ num_traits::CheckedAdd
{
/// Create a new UpDownOrder with the given up and down values.
pub fn new(up: T, down: T) -> Self {
Self {
up, down
}
}
/// Return a new UpDownOrder with both up and down set to zero.
pub fn zeroed() -> Self {
Self { down: T::zero(), up: T::zero() }
}
/// Check if both up and down are less than the given limit.
pub fn both_less_than(&self, limit: T) -> bool {
self.down < limit && self.up < limit
}
/// Subtract the given UpDownOrder from this one, returning a new UpDownOrder.
/// If the subtraction would result in a negative value, the result is set to zero.
pub fn checked_sub_or_zero(&self, rhs: UpDownOrder<T>) -> UpDownOrder<T> {
let down = T::checked_sub(&self.down, &rhs.down).unwrap_or(T::zero());
let up = T::checked_sub(&self.up, &rhs.up).unwrap_or(T::zero());
UpDownOrder { down, up }
}
/// Add the given UpDownOrder to this one, updating the values in place.
/// Overflowing values are set to zero.
pub fn checked_add(&mut self, rhs: UpDownOrder<T>) {
self.down = self.down.checked_add(&rhs.down).unwrap_or(T::zero());
self.up = self.up.checked_add(&rhs.up).unwrap_or(T::zero());
}
}
impl <T> Into<DownUpOrder<T>> for UpDownOrder<T> {
fn into(self) -> DownUpOrder<T> {
DownUpOrder {
up: self.down,
down: self.up,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reverse() {
let a = DownUpOrder::new(1, 2);
let b: UpDownOrder<i32> = a.into();
assert_eq!(a.down, b.up);
}
#[test]
fn test_checked_sub() {
let a = UpDownOrder::new(1u64, 1);
let b= UpDownOrder::new(1, 1);
let c = a.checked_sub_or_zero(b);
assert_eq!(c.up, 0);
assert_eq!(c.down, 0);
let b= UpDownOrder::new(2, 2);
let c = a.checked_sub_or_zero(b);
assert_eq!(c.up, 0);
assert_eq!(c.down, 0);
}
#[test]
fn test_checked_add() {
let mut a = UpDownOrder::new(u64::MAX, u64::MAX);
let b = UpDownOrder::new(1, 1);
a.checked_add(b);
assert_eq!(a.down, 0);
assert_eq!(a.up, 0);
let mut a = UpDownOrder::new(1, 2);
a.checked_add(b);
assert_eq!(a.down, 3);
assert_eq!(a.up, 2);
}
}

View File

@ -1,3 +1,4 @@
use std::fmt::Display;
use byteorder::{BigEndian, ByteOrder};
use zerocopy::FromBytes;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
@ -58,7 +59,7 @@ impl XdpIpAddress {
&& self.0[11] == 0xFF
}
/// Convers an `XdpIpAddress` type to a Rust `IpAddr` type, using
/// Converts an `XdpIpAddress` type to a Rust `IpAddr` type, using
/// the in-build mapped function for squishing IPv4 into IPv6
pub fn as_ipv6(&self) -> Ipv6Addr {
if self.is_v4() {
@ -99,6 +100,13 @@ impl XdpIpAddress {
}
}
impl Display for XdpIpAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ip())
}
}
impl From<XdpIpAddress> for IpAddr {
fn from(val: XdpIpAddress) -> Self {
val.as_ip()

View File

@ -9,36 +9,43 @@ default = ["equinix_tests"]
equinix_tests = []
[dependencies]
anyhow = "1"
anyhow = { workspace = true }
lqos_config = { path = "../lqos_config" }
lqos_sys = { path = "../lqos_sys" }
lqos_queue_tracker = { path = "../lqos_queue_tracker" }
lqos_utils = { path = "../lqos_utils" }
lqos_heimdall = { path = "../lqos_heimdall" }
lts_client = { path = "../lts_client" }
lqos_support_tool = { path = "../lqos_support_tool" }
tokio = { version = "1", features = [ "full", "parking_lot" ] }
once_cell = "1.17.1"
once_cell = { workspace = true}
lqos_bus = { path = "../lqos_bus" }
signal-hook = "0.3"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = { workspace = true }
serde = { workspace = true }
env_logger = "0"
log = "0"
nix = "0"
sysinfo = "0"
dashmap = "5"
num-traits = "0.2"
thiserror = "1"
log = { workspace = true }
nix = { workspace = true }
sysinfo = { workspace = true }
dashmap = { workspace = true }
itertools = "0.12.1"
csv = "1"
reqwest = { version = "0.11.24", features = ["blocking"] }
csv = { workspace = true }
reqwest = { workspace = true }
flate2 = "1.0"
bincode = "1"
ip_network_table = "0"
ip_network = "0"
zerocopy = {version = "0.6.1", features = [ "simd" ] }
bincode = { workspace = true }
ip_network_table = { workspace = true }
ip_network = { workspace = true }
zerocopy = { workspace = true }
fxhash = "0.2.1"
axum = { version = "0.7.5", features = ["ws", "http2"] }
axum-extra = { version = "0.9.3", features = ["cookie", "cookie-private"] }
tower-http = { version = "0.5.2", features = ["fs"] }
strum = { version = "0.26.3", features = ["derive"] }
default-net = { workspace = true }
surge-ping = "0.8.1"
rand = "0.8.5"
mime_guess = "2.0.4"
# Support JemAlloc on supported platforms
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
jemallocator = "0.5"
jemallocator = { workspace = true }

9
src/rust/lqosd/copy_files.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
echo "Copying static"
cp -v -R src/node_manager/static2/* ../../bin/static2/
echo "Done"
pushd src/node_manager/js_build || exit
./esbuild.sh
popd || exit
cp -R src/node_manager/js_build/out/* ../../bin/static2/

View File

@ -4,7 +4,7 @@ use std::{time::Duration, net::TcpStream, io::Write};
use lqos_bus::anonymous::{AnonymousUsageV1, build_stats};
use lqos_sys::num_possible_cpus;
use sysinfo::System;
use crate::{shaped_devices_tracker::{SHAPED_DEVICES, NETWORK_JSON}, stats::{HIGH_WATERMARK_DOWN, HIGH_WATERMARK_UP}};
use crate::{shaped_devices_tracker::{SHAPED_DEVICES, NETWORK_JSON}, stats::HIGH_WATERMARK};
const SLOW_START_SECS: u64 = 1;
const INTERVAL_SECS: u64 = 60 * 60 * 24;
@ -71,11 +71,11 @@ fn anonymous_usage_dump() -> anyhow::Result<()> {
data.git_hash = env!("GIT_HASH").to_string();
data.shaped_device_count = SHAPED_DEVICES.read().unwrap().devices.len();
data.net_json_len = NETWORK_JSON.read().unwrap().nodes.len();
data.net_json_len = NETWORK_JSON.read().unwrap().get_nodes_when_ready().len();
data.high_watermark_bps = (
HIGH_WATERMARK_DOWN.load(std::sync::atomic::Ordering::Relaxed),
HIGH_WATERMARK_UP.load(std::sync::atomic::Ordering::Relaxed),
HIGH_WATERMARK.get_down(),
HIGH_WATERMARK.get_up(),
);

View File

@ -8,7 +8,7 @@ use lts_client::{
pub(crate) fn get_network_tree() -> Vec<(usize, NetworkTreeEntry)> {
if let Ok(reader) = NETWORK_JSON.read() {
let result = reader
.nodes
.get_nodes_when_ready()
.iter()
.enumerate()
.map(|(idx, n)| (idx, n.into()))

View File

@ -28,11 +28,12 @@ use signal_hook::{
consts::{SIGHUP, SIGINT, SIGTERM},
iterator::Signals,
};
use stats::{BUS_REQUESTS, TIME_TO_POLL_HOSTS, HIGH_WATERMARK_DOWN, HIGH_WATERMARK_UP, FLOWS_TRACKED};
use stats::{BUS_REQUESTS, TIME_TO_POLL_HOSTS, HIGH_WATERMARK, FLOWS_TRACKED};
use throughput_tracker::flow_data::get_rtt_events_per_second;
use tokio::join;
mod stats;
mod preflight_checks;
mod node_manager;
// Use JemAllocator only on supported platforms
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@ -64,7 +65,6 @@ async fn main() -> Result<()> {
// Load config
let config = lqos_config::load_config()?;
// Apply Tunings
tuning::tune_lqosd_from_config_file()?;
@ -137,6 +137,13 @@ async fn main() -> Result<()> {
// Create the socket server
let server = UnixSocketServer::new().expect("Unable to spawn server");
// Webserver starting point
tokio::spawn(async {
if let Err(e) = node_manager::spawn_webserver().await {
log::error!("Node Manager Failed: {e:?}");
}
});
// Main bus listen loop
server.listen(handle_bus_requests).await?;
Ok(())
@ -177,7 +184,7 @@ fn handle_bus_requests(
BusRequest::ClearIpFlow => clear_ip_flows(),
BusRequest::ListIpFlow => list_mapped_ips(),
BusRequest::XdpPping => throughput_tracker::xdp_pping_compat(),
BusRequest::RttHistogram => throughput_tracker::rtt_histogram(),
BusRequest::RttHistogram => throughput_tracker::rtt_histogram::<20>(),
BusRequest::HostCounts => throughput_tracker::host_counts(),
BusRequest::AllUnknownIps => throughput_tracker::all_unknown_ips(),
BusRequest::ReloadLibreQoS => program_control::reload_libre_qos(),
@ -217,10 +224,7 @@ fn handle_bus_requests(
BusResponse::LqosdStats {
bus_requests: BUS_REQUESTS.load(std::sync::atomic::Ordering::Relaxed),
time_to_poll_hosts: TIME_TO_POLL_HOSTS.load(std::sync::atomic::Ordering::Relaxed),
high_watermark: (
HIGH_WATERMARK_DOWN.load(std::sync::atomic::Ordering::Relaxed),
HIGH_WATERMARK_UP.load(std::sync::atomic::Ordering::Relaxed),
),
high_watermark: HIGH_WATERMARK.as_down_up(),
tracked_flows: FLOWS_TRACKED.load(std::sync::atomic::Ordering::Relaxed),
rtt_events_per_second: get_rtt_events_per_second(),
}

Some files were not shown because too many files have changed in this diff Show More