This is a large commit that activates UI2 as the default.

* Remove `lqos_node_manager` project.
* Update the project manifest.
* Update `build_rust.sh` to no longer build the old node manager, and disable it if it were running as a service.
* Update `build_dpkg.sh` to no longer build the old node manager. Added lines to stop and disable the service if it were created by a previous version.
This commit is contained in:
Herbert Wolverson 2024-07-23 12:19:53 -05:00
parent b851fab0a3
commit 1240b43052
52 changed files with 35 additions and 8359 deletions

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,8 +3,7 @@
# 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`
@ -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,15 +64,11 @@ done
popd > /dev/null
# Copy the node manager's static web content
cp -R rust/lqos_node_manager/static/* bin/static
mkdir -p bin/static2/vendor
pushd rust/lqosd > /dev/null
./copy_files.sh
popd > /dev/null
# Copy Rocket.toml to tell the node manager where to listen
cp rust/lqos_node_manager/Rocket.toml bin/
# Copy the Python library for LibreQoS.py et al.
pushd rust/lqos_python > /dev/null
cargo build $BUILD_FLAGS
@ -91,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
@ -99,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!"

560
src/rust/Cargo.lock generated
View File

@ -73,21 +73,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
@ -170,42 +155,6 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "async-compression"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5"
dependencies = [
"brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-stream"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "async-trait"
version = "0.1.80"
@ -217,21 +166,6 @@ dependencies = [
"syn",
]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "atomic"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
dependencies = [
"bytemuck",
]
[[package]]
name = "autocfg"
version = "1.3.0"
@ -395,12 +329,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "binascii"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
[[package]]
name = "bincode"
version = "1.3.3"
@ -454,39 +382,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
[[package]]
name = "byteorder"
version = "1.5.0"
@ -961,39 +862,6 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "devise"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8"
dependencies = [
"devise_codegen",
"devise_core",
]
[[package]]
name = "devise_codegen"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6"
dependencies = [
"devise_core",
"quote",
]
[[package]]
name = "devise_core"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a"
dependencies = [
"bitflags 2.6.0",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -1015,18 +883,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "dns-lookup"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872"
dependencies = [
"cfg-if",
"libc",
"socket2 0.4.10",
"winapi",
]
[[package]]
name = "dryoc"
version = "0.5.3"
@ -1114,20 +970,6 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "figment"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic 0.6.0",
"pear",
"serde",
"toml",
"uncased",
"version_check",
]
[[package]]
name = "filetime"
version = "0.2.23"
@ -1188,7 +1030,6 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
@ -1211,34 +1052,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-executor"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
@ -1257,10 +1076,8 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@ -1278,19 +1095,6 @@ dependencies = [
"byteorder",
]
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@ -1509,7 +1313,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.5.7",
"socket2",
"tokio",
"tower-service",
"tracing",
@ -1604,7 +1408,6 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
"serde",
]
[[package]]
@ -1613,12 +1416,6 @@ version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "inlinable_string"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
[[package]]
name = "inotify"
version = "0.9.6"
@ -1836,21 +1633,6 @@ dependencies = [
"log",
]
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"serde",
"serde_json",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "lqos_anonymous_stats_server"
version = "0.1.0"
@ -1924,27 +1706,6 @@ dependencies = [
"lqos_sys",
]
[[package]]
name = "lqos_node_manager"
version = "0.1.0"
dependencies = [
"anyhow",
"dashmap",
"default-net",
"dns-lookup",
"jemallocator",
"lqos_bus",
"lqos_config",
"lqos_support_tool",
"lqos_utils",
"nix 0.29.0",
"once_cell",
"reqwest",
"rocket",
"rocket_async_compression",
"sysinfo",
]
[[package]]
name = "lqos_python"
version = "0.1.0"
@ -2149,15 +1910,6 @@ dependencies = [
"uisp",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "matchit"
version = "0.7.3"
@ -2228,25 +1980,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http 1.1.0",
"httparse",
"memchr",
"mime",
"spin",
"tokio",
"tokio-util",
"version_check",
]
[[package]]
name = "native-tls"
version = "0.2.12"
@ -2518,29 +2251,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pear"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
dependencies = [
"inlinable_string",
"pear_codegen",
"yansi",
]
[[package]]
name = "pear_codegen"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -2704,19 +2414,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn",
"version_check",
"yansi",
]
[[package]]
name = "pyo3"
version = "0.20.3"
@ -2877,26 +2574,6 @@ dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "ref-cast"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "regex"
version = "1.10.5"
@ -2905,17 +2582,8 @@ checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@ -2926,15 +2594,9 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.4",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.4"
@ -2981,126 +2643,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "rocket"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
dependencies = [
"async-stream",
"async-trait",
"atomic 0.5.3",
"binascii",
"bytes",
"either",
"figment",
"futures",
"indexmap",
"log",
"memchr",
"multer",
"num_cpus",
"parking_lot",
"pin-project-lite",
"rand",
"ref-cast",
"rmp-serde",
"rocket_codegen",
"rocket_http",
"serde",
"serde_json",
"state",
"tempfile",
"time",
"tokio",
"tokio-stream",
"tokio-util",
"ubyte",
"uuid",
"version_check",
"yansi",
]
[[package]]
name = "rocket_async_compression"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "321fc3e2ddccf03773c873ceea8e699556c54fa1fa8c7ca3bcf8a9ca283f67ba"
dependencies = [
"async-compression",
"futures",
"lazy_static",
"log",
"rocket",
]
[[package]]
name = "rocket_codegen"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
dependencies = [
"devise",
"glob",
"indexmap",
"proc-macro2",
"quote",
"rocket_http",
"syn",
"unicode-xid",
"version_check",
]
[[package]]
name = "rocket_http"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
dependencies = [
"cookie",
"either",
"futures",
"http 0.2.12",
"hyper 0.14.29",
"indexmap",
"log",
"memchr",
"pear",
"percent-encoding",
"pin-project-lite",
"ref-cast",
"serde",
"smallvec",
"stable-pattern",
"state",
"time",
"tokio",
"uncased",
"uuid",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
@ -3183,12 +2725,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -3378,16 +2914,6 @@ version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "socket2"
version = "0.5.7"
@ -3398,12 +2924,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "sqlite"
version = "0.30.5"
@ -3444,24 +2964,6 @@ dependencies = [
"syn",
]
[[package]]
name = "stable-pattern"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
dependencies = [
"memchr",
]
[[package]]
name = "state"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
dependencies = [
"loom",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -3512,7 +3014,7 @@ dependencies = [
"parking_lot",
"pnet_packet",
"rand",
"socket2 0.5.7",
"socket2",
"thiserror",
"tokio",
"tracing",
@ -3695,7 +3197,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.7",
"socket2",
"tokio-macros",
"windows-sys 0.48.0",
]
@ -3721,17 +3223,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-tungstenite"
version = "0.21.0"
@ -3894,14 +3385,10 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
@ -3937,15 +3424,6 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ubyte"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
dependencies = [
"serde",
]
[[package]]
name = "uisp"
version = "0.1.0"
@ -3976,16 +3454,6 @@ dependencies = [
"uisp",
]
[[package]]
name = "uncased"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
dependencies = [
"serde",
"version_check",
]
[[package]]
name = "unicase"
version = "2.7.0"
@ -4038,12 +3506,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "unindent"
version = "0.2.3"
@ -4091,7 +3553,6 @@ checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
dependencies = [
"getrandom",
"rand",
"serde",
]
[[package]]
@ -4528,15 +3989,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
dependencies = [
"is-terminal",
]
[[package]]
name = "zerocopy"
version = "0.6.6"

View File

@ -22,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

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 = { workspace = true }
sysinfo = { workspace = true }
default-net = { workspace = true }
nix = { workspace = true }
once_cell = { workspace = true}
dns-lookup = "1"
dashmap = { workspace = true }
reqwest = { workspace = true }
lqos_support_tool = { path = "../lqos_support_tool" }
# Support JemAlloc on supported platforms
[target.'cfg(any(target_arch = "x86", target_arch = "x86_64"))'.dependencies]
jemallocator = { workspace = true }

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: (high_watermark.down, high_watermark.up),
tracked_flows,
rtt_events_per_second,
}));
}
}
NoCache::new(Json(LqosStats::default()))
}

View File

@ -1,101 +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(),
};
let result = result
.into_iter()
.map(|(name, bytes, rtt)| (name, [bytes.down, bytes.up], rtt) )
.collect();
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(),
};
let result = result.
into_iter()
.map(|(name, bytes)| (name, (bytes.down, bytes.up)))
.collect();
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, bytes) 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: (bytes.down, bytes.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, bytes) 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(), bytes.down, bytes.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,62 +0,0 @@
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use rocket::fs::NamedFile;
use rocket::serde::Deserialize;
use rocket::serde::json::Json;
use lqos_config::load_config;
use lqos_support_tool::{run_sanity_checks, SanityChecks};
use crate::auth_guard::AuthGuard;
#[get("/api/sanity")]
pub async fn run_sanity_check(
_auth: AuthGuard,
) -> Json<SanityChecks> {
let mut status = run_sanity_checks().unwrap();
status.results.sort_by(|a,b| a.success.cmp(&b.success));
Json(status)
}
#[derive(Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct SupportMetadata {
name: String,
comment: String,
}
#[post("/api/gatherSupport", data="<info>")]
pub async fn gather_support_data(
info: Json<SupportMetadata>
) -> NamedFile {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let filename = format!("/tmp/libreqos_{}.support", timestamp);
let path = Path::new(&filename);
let lts_key = if let Ok(cfg) = load_config() {
cfg.long_term_stats.license_key.unwrap_or("None".to_string())
} else {
"None".to_string()
};
if let Ok(dump) = lqos_support_tool::gather_all_support_info(&info.name, &info.comment, &lts_key) {
std::fs::write(&path, dump.serialize_and_compress().unwrap()).unwrap();
}
NamedFile::open(path).await.unwrap()
}
#[post("/api/submitSupport", data="<info>")]
pub async fn submit_support_data(
info: Json<SupportMetadata>
) -> String {
let lts_key = if let Ok(cfg) = load_config() {
cfg.long_term_stats.license_key.unwrap_or("None".to_string())
} else {
"None".to_string()
};
if let Ok(dump) = lqos_support_tool::gather_all_support_info(&info.name, &info.comment, &lts_key) {
lqos_support_tool::submit_to_network(dump);
"Your support submission has been sent".to_string()
} else {
"Something went wrong".to_string()
}
}

View File

@ -1,105 +0,0 @@
use lqos_config::load_config;
use lqos_utils::unix_time::unix_now;
use rocket::serde::json::Json;
use rocket::serde::{Deserialize, Serialize};
static LAST_VERSION_CHECK: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
const ONE_HOUR_SECONDS: u64 = 60 * 60;
const VERSION_STRING: &str = include_str!("../../../VERSION_STRING");
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct VersionCheckRequest {
current_git_hash: String,
version_string: String,
node_id: String,
}
#[derive(Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct VersionCheckResponse {
update_available: bool,
}
async fn send_version_check() -> anyhow::Result<VersionCheckResponse> {
if let Ok(cfg) = load_config() {
let current_hash = env!("GIT_HASH");
let request = VersionCheckRequest {
current_git_hash: current_hash.to_string(),
version_string: VERSION_STRING.to_string(),
node_id: cfg.node_id.to_string(),
};
let response = reqwest::Client::new()
.post("https://stats.libreqos.io/api/version_check")
.json(&request)
.send()
.await?
.json()
.await?;
Ok(response)
} else {
anyhow::bail!("No config");
}
}
#[get("/api/version_check")]
pub async fn version_check() -> Json<String> {
let last_check = LAST_VERSION_CHECK.load(std::sync::atomic::Ordering::Relaxed);
if let Ok(now) = unix_now() {
if now > last_check + ONE_HOUR_SECONDS {
let res = send_version_check().await;
if let Ok(response) = send_version_check().await {
LAST_VERSION_CHECK.store(now, std::sync::atomic::Ordering::Relaxed);
if response.update_available {
return Json(String::from("Update available"));
}
} else {
error!("Unable to send version check");
error!("{res:?}");
}
}
}
Json(String::from("All Good"))
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub enum StatsCheckResponse {
DoNothing,
NotSetup,
GoodToGo,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct StatsCheckAction {
action: StatsCheckResponse,
node_id: String,
}
#[get("/api/stats_check")]
pub async fn stats_check() -> Json<StatsCheckAction> {
let mut response = StatsCheckAction {
action: StatsCheckResponse::DoNothing,
node_id: String::new(),
};
if let Ok(cfg) = load_config() {
if !cfg.long_term_stats.gather_stats {
response = StatsCheckAction {
action: StatsCheckResponse::NotSetup,
node_id: cfg.node_id.to_string(),
};
} else {
// Stats are enabled
response = StatsCheckAction {
action: StatsCheckResponse::GoodToGo,
node_id: cfg.node_id.to_string(),
};
}
}
Json(response)
}

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.down, bits_per_second.up);
lock.data[head].packets_per_second = (packets_per_second.down, packets_per_second.up);
lock.data[head].shaped_bits_per_second = (shaped_bits_per_second.down, shaped_bits_per_second.up);
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.down, i.bits_per_second.up),
packets_per_second: (i.packets_per_second.down, i.packets_per_second.up),
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.down, i.tcp_retransmits.up),
};
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.down, unknown.bits_per_second.up);
}
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

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,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.08 323.96">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="M137.98,0c2.33,0,4.67,0,7,0,.98,.83,1.87,1.82,2.97,2.45,42.65,24.58,85.27,49.2,128.04,73.58,5.18,2.95,7.14,6.46,7.1,12.35-.3,48.79-.51,97.59-.3,146.38,.03,6.9-2.51,10.56-8.15,13.78-33.8,19.28-67.47,38.78-101.14,58.3-9.59,5.56-19.01,11.4-28.51,17.12h-7c-7.85-3.9-15.98-7.33-23.51-11.79-32.3-19.11-64.46-38.46-96.57-57.89C11.71,250.54,5.96,246.09,0,241.97,0,188.31,0,134.65,0,80.99c1.99-1.42,3.87-3.03,5.97-4.25C42.03,55.88,78.12,35.09,114.17,14.22,122.17,9.59,130.05,4.75,137.98,0Zm129.74,98.28c-13.51,16.02-26.06,31.09-38.87,45.93-2.62,3.04-3.49,6.04-3.46,9.91,.16,15.99,.18,31.98-.02,47.97-.05,3.64,1.09,5.72,4.25,7.48,11.77,6.56,23.4,13.38,35.1,20.06,.89,.51,1.94,.75,3,1.15V98.28Zm-9.62-13.78c.04-.43,.08-.86,.11-1.29-37.74-21.74-75.47-43.49-113.95-65.66,0,16.25-.09,31.22,.13,46.19,.02,1.54,1.51,3.65,2.91,4.47,15.04,8.86,30.17,17.56,45.38,26.14,1.51,.85,3.73,1.06,5.49,.78,12.09-1.93,24.16-4.03,36.21-6.18,7.92-1.41,15.81-2.97,23.71-4.46ZM15.06,98.41V231.35c14-8.08,27.4-15.71,40.64-23.6,1.15-.69,1.85-3.02,1.86-4.6,.13-16.98,.12-33.96,.02-50.94-.01-1.69-.44-3.76-1.47-5-13.36-16.05-26.87-31.99-41.06-48.81Zm2.82,138.31c38.75,22.38,76.47,44.16,115.44,66.67-7.56-21.03-14.52-40.52-21.64-59.96-.5-1.37-1.96-2.63-3.29-3.41-15.07-8.8-30.18-17.54-45.35-26.16-1.22-.69-3.37-1.12-4.44-.52-13.31,7.48-26.51,15.17-40.72,23.38Zm246.92,0c-13.5-7.81-26.09-14.91-38.45-22.38-3.11-1.88-5.31-1.48-8.13,.17-14.48,8.49-29.07,16.79-43.53,25.32-1.69,1-3.36,2.82-4.03,4.63-6.61,17.9-13.01,35.87-19.44,53.83-.45,1.27-.7,2.62-1.25,4.77,38.52-22.25,76.18-44.01,114.82-66.33ZM23.57,83.77c2.22,.69,3.26,1.15,4.35,1.34,18.89,3.38,37.79,6.78,56.7,10.02,1.74,.3,3.96,.09,5.46-.76,14.93-8.4,29.73-17.02,44.62-25.49,2.4-1.37,3.53-2.92,3.5-5.88-.17-13.82-.06-27.65-.1-41.47,0-1.03-.34-2.05-.59-3.44C99.6,39.94,62.17,61.52,23.57,83.77Zm225.87,9.21c-.1-.19-.2-.38-.3-.57-11.23,1.94-22.48,3.76-33.67,5.87-7.3,1.37-15.23,1.75-21.63,5.02-15.98,8.15-31.27,17.67-46.78,26.73-1.14,.67-2.6,2.05-2.64,3.14-.25,6.42-.12,12.86-.12,20.38,35.63-20.52,70.39-40.54,105.15-60.57Zm-99.23,70.53c5.74,3.36,10.66,6.12,15.44,9.09,2.57,1.6,4.64,1.48,7.31-.1,14.18-8.39,28.54-16.48,42.72-24.86,2.95-1.74,5.73-4.02,8.02-6.56,5.56-6.17,10.81-12.63,16.16-19,4.92-5.87,9.8-11.78,14.7-17.67-.25-.26-.5-.52-.75-.77-34.23,19.79-68.46,39.58-103.59,59.88Zm-2.63,124.5l.86,.16c5.78-16.02,11.56-32.04,17.3-48.08,.27-.75,.21-1.65,.21-2.47,.01-18.31,.04-36.62-.04-54.93,0-1.31-.53-3.23-1.47-3.82-5.28-3.31-10.76-6.31-16.86-9.81v118.95Zm-13.09,.76c.24-.1,.47-.21,.71-.31v-119.41c-5.37,3.03-9.89,5.78-14.59,8.16-2.89,1.46-3.96,3.31-3.91,6.62,.21,13.15,.46,26.32-.03,39.45-.41,11,1.26,21.36,5.41,31.57,4.53,11.15,8.31,22.59,12.42,33.91Zm-2.08-125.22c-35.12-20.32-69.59-40.27-104.05-60.21,7.86,10.87,16.28,20.91,24.85,30.84,3.56,4.12,6.79,9,11.26,11.79,15.08,9.39,30.64,18.04,46.08,26.85,1.27,.72,3.42,1.27,4.52,.69,5.7-2.99,11.19-6.37,17.34-9.96Zm5.7-10.18c0-6.55-.25-12.16,.09-17.73,.21-3.4-1.23-5.08-3.94-6.61-12.6-7.11-25.23-14.18-37.62-21.65-6.07-3.66-12.27-6.24-19.33-7.34-14.48-2.25-28.88-5.03-43.31-7.59-.09,.27-.17,.53-.26,.8,34.51,19.88,69.02,39.76,104.37,60.12Zm36.27,25.14c15.23,8.81,29.68,17.18,44.81,25.93v-51.81c-14.97,8.64-29.42,16.99-44.81,25.88Zm-64.05,3.73c-15.27,8.83-29.76,17.21-44.85,25.93,15.34,8.87,29.77,17.21,44.85,25.93v-51.86Zm61.97-.05v52.04c15.04-8.77,29.51-17.21,44.81-26.13-15.45-8.94-29.8-17.23-44.81-25.91Zm-108.52,22.08c14.91-8.63,29.44-17.04,44.51-25.77-15.03-8.69-29.36-16.97-44.51-25.72v51.49ZM138.74,73.37c-14.99,8.66-29.36,16.97-44.57,25.76,15.31,8.81,29.72,17.09,44.57,25.63v-51.38Zm49.81,25.73c-15.29-8.81-29.7-17.11-44.55-25.67v51.29c14.96-8.6,29.31-16.86,44.55-25.62Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

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

@ -15,7 +15,7 @@ use crate::node_manager::local_api::local_api;
/// with tokio::spawn unless you want it to block execution.
pub async fn spawn_webserver() -> Result<()> {
// TODO: port change is temporary
let listener = TcpListener::bind(":::9223").await?;
let listener = TcpListener::bind(":::9123").await?;
// Check that static content is available and setup the path
let config = load_config()?;