Merge branch 'v1.4-pre-alpha-rust-integration'

This commit is contained in:
Herbert Wolverson
2023-01-13 13:57:47 +00:00
141 changed files with 15080 additions and 38 deletions

20
.github/workflows/rust.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Rust
on: [push]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt-get update; sudo apt-get install --no-install-recommends python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`uname r` libbpf-dev
if: matrix.os == 'ubuntu-latest'
- name: Build
run: pushd src/rust; cargo build --verbose --all; popd
- name: Run tests
run: pushd src/rust; cargo test --verbose --all; popd

18
.gitignore vendored
View File

@@ -37,6 +37,8 @@ src/ShapedDevices.csv
src/ShapedDevices.lastLoaded.csv
src/network.json
src/ispConfig.py
src/ispConfig.py.backup
src/ispConfig.py.test
src/statsByCircuit.json
src/statsByParentNode.json
src/lastGoodConfig.json
@@ -45,6 +47,22 @@ src/mikrotikDHCPRouterList.csv
src/longTermStats.json
src/queuingStructure.json
src/tinsStats.json
src/linux_tc.txt
src/lastRun.txt
src/liblqos_python.so
src/webusers.toml
# Ignore Rust build artifacts
src/rust/target
src/bin/static
src/bin/lqosd
src/bin/lqtop
src/bin/xdp_iphash_to_cpu_cmdline
src/bin/xdp_pping
src/bin/lqos_node_manager
src/bin/webusers
src/bin/Rocket.toml
# Ignore project folders for some IDEs
.idea/

4
.gitmodules vendored
View File

@@ -13,6 +13,4 @@
[submodule "v1.2/xdp-cpumap-tc"]
path = old/v1.2/xdp-cpumap-tc
url = https://github.com/xdp-project/xdp-cpumap-tc.git
[submodule "v1.3/cpumap-pping"]
path = src/cpumap-pping
url = https://github.com/thebracket/cpumap-pping.git

View File

@@ -5,6 +5,7 @@ import csv
import io
import ipaddress
import json
import math
import os
import os.path
import subprocess
@@ -20,7 +21,10 @@ import binpacking
from ispConfig import sqm, upstreamBandwidthCapacityDownloadMbps, upstreamBandwidthCapacityUploadMbps, \
interfaceA, interfaceB, enableActualShellCommands, useBinPackingToBalanceCPU, monitorOnlyMode, \
runShellCommandsAsSudo, generatedPNDownloadMbps, generatedPNUploadMbps, queuesAvailableOverride
runShellCommandsAsSudo, generatedPNDownloadMbps, generatedPNUploadMbps, queuesAvailableOverride, \
OnAStick
from liblqos_python import is_lqosd_alive, clear_ip_mappings, delete_ip_mapping
# Automatically account for TCP overhead of plans. For example a 100Mbps plan needs to be set to 109Mbps for the user to ever see that result on a speed test
# Does not apply to nodes of any sort, just endpoint devices
@@ -80,9 +84,8 @@ def tearDown(interfaceA, interfaceB):
# Full teardown of everything for exiting LibreQoS
if enableActualShellCommands:
# Clear IP filters and remove xdp program from interfaces
result = os.system('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --clear')
shell('ip link set dev ' + interfaceA + ' xdp off')
shell('ip link set dev ' + interfaceB + ' xdp off')
#result = os.system('./bin/xdp_iphash_to_cpu_cmdline clear')
clear_ip_mappings() # Use the bus
clearPriorSettings(interfaceA, interfaceB)
def findQueuesAvailable():
@@ -440,7 +443,12 @@ def refreshShapers():
# Pull rx/tx queues / CPU cores available
queuesAvailable = findQueuesAvailable()
stickOffset = 0
if OnAStick:
print("On-a-stick override dividing queues")
# The idea here is that download use queues 0 - n/2, upload uses the other half
queuesAvailable = math.floor(queuesAvailable / 2)
stickOffset = queuesAvailable
# If in monitorOnlyMode, override network.json bandwidth rates to where no shaping will actually occur
if monitorOnlyMode == True:
@@ -527,14 +535,18 @@ def refreshShapers():
# Track minor counter by CPU. This way we can have > 32000 hosts (htb has u16 limit to minor handle)
for x in range(queuesAvailable):
minorByCPUpreloaded[x+1] = 3
def traverseNetwork(data, depth, major, minorByCPU, queue, parentClassID, parentMaxDL, parentMaxUL):
def traverseNetwork(data, depth, major, minorByCPU, queue, parentClassID, upParentClassID, parentMaxDL, parentMaxUL):
for node in data:
circuitsForThisNetworkNode = []
nodeClassID = hex(major) + ':' + hex(minorByCPU[queue])
upNodeClassID = hex(major+stickOffset) + ':' + hex(minorByCPU[queue])
data[node]['classid'] = nodeClassID
data[node]['up_classid'] = upNodeClassID
if depth == 0:
parentClassID = hex(major) + ':'
upParentClassID = hex(major+stickOffset) + ':'
data[node]['parentClassID'] = parentClassID
data[node]['up_parentClassID'] = upParentClassID
# If in monitorOnlyMode, override bandwidth rates to where no shaping will actually occur
if monitorOnlyMode == True:
data[node]['downloadBandwidthMbps'] = 10000
@@ -551,8 +563,10 @@ def refreshShapers():
data[node]['uploadBandwidthMbpsMin'] = round(data[node]['uploadBandwidthMbps']*.95)
data[node]['classMajor'] = hex(major)
data[node]['up_classMajor'] = hex(major + stickOffset)
data[node]['classMinor'] = hex(minorByCPU[queue])
data[node]['cpuNum'] = hex(queue-1)
data[node]['up_cpuNum'] = hex(queue-1+stickOffset)
thisParentNode = {
"parentNodeName": node,
"classID": nodeClassID,
@@ -571,7 +585,10 @@ def refreshShapers():
warnings.warn("uploadMax of Circuit ID [" + circuit['circuitID'] + "] exceeded that of its parent node. Reducing to that of its parent node now.", stacklevel=2)
parentString = hex(major) + ':'
flowIDstring = hex(major) + ':' + hex(minorByCPU[queue])
upFlowIDstring = hex(major + stickOffset) + ':' + hex(minorByCPU[queue])
circuit['classid'] = flowIDstring
circuit['up_classid'] = upFlowIDstring
print("Added up_classid to circuit: " + circuit['up_classid'])
# Create circuit dictionary to be added to network structure, eventually output as queuingStructure.json
maxDownload = min(circuit['maxDownload'],data[node]['downloadBandwidthMbps'])
maxUpload = min(circuit['maxUpload'],data[node]['uploadBandwidthMbps'])
@@ -587,7 +604,9 @@ def refreshShapers():
"ParentNode": circuit['ParentNode'],
"devices": circuit['devices'],
"classid": flowIDstring,
"up_classid" : upFlowIDstring,
"classMajor": hex(major),
"up_classMajor" : hex(major + stickOffset),
"classMinor": hex(minorByCPU[queue]),
"comment": circuit['comment']
}
@@ -601,7 +620,7 @@ def refreshShapers():
if 'children' in data[node]:
# We need to keep tabs on the minor counter, because we can't have repeating class IDs. Here, we bring back the minor counter from the recursive function
minorByCPU[queue] = minorByCPU[queue] + 1
minorByCPU = traverseNetwork(data[node]['children'], depth+1, major, minorByCPU, queue, nodeClassID, data[node]['downloadBandwidthMbps'], data[node]['uploadBandwidthMbps'])
minorByCPU = traverseNetwork(data[node]['children'], depth+1, major, minorByCPU, queue, nodeClassID, upNodeClassID, data[node]['downloadBandwidthMbps'], data[node]['uploadBandwidthMbps'])
# If top level node, increment to next queue / cpu core
if depth == 0:
if queue >= queuesAvailable:
@@ -612,8 +631,7 @@ def refreshShapers():
major += 1
return minorByCPU
# Here is the actual call to the recursive traverseNetwork() function. finalMinor is not used.
minorByCPU = traverseNetwork(network, 0, major=1, minorByCPU=minorByCPUpreloaded, queue=1, parentClassID=None, parentMaxDL=upstreamBandwidthCapacityDownloadMbps, parentMaxUL=upstreamBandwidthCapacityUploadMbps)
minorByCPU = traverseNetwork(network, 0, major=1, minorByCPU=minorByCPUpreloaded, queue=1, parentClassID=None, upParentClassID=None, parentMaxDL=upstreamBandwidthCapacityDownloadMbps, parentMaxUL=upstreamBandwidthCapacityUploadMbps)
linuxTCcommands = []
xdpCPUmapCommands = []
@@ -639,23 +657,25 @@ def refreshShapers():
command = 'qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':2 ' + sqm
linuxTCcommands.append(command)
# Note the use of stickOffset, and not replacing the root queue if we're on a stick
thisInterface = interfaceB
logging.info("# MQ Setup for " + thisInterface)
command = 'qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq'
linuxTCcommands.append(command)
if not OnAStick:
command = 'qdisc replace dev ' + thisInterface + ' root handle 7FFF: mq'
linuxTCcommands.append(command)
for queue in range(queuesAvailable):
command = 'qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+1) + ' handle ' + hex(queue+1) + ': htb default 2'
command = 'qdisc add dev ' + thisInterface + ' parent 7FFF:' + hex(queue+stickOffset+1) + ' handle ' + hex(queue+stickOffset+1) + ': htb default 2'
linuxTCcommands.append(command)
command = 'class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ': classid ' + hex(queue+1) + ':1 htb rate '+ str(upstreamBandwidthCapacityUploadMbps) + 'mbit ceil ' + str(upstreamBandwidthCapacityUploadMbps) + 'mbit'
command = 'class add dev ' + thisInterface + ' parent ' + hex(queue+stickOffset+1) + ': classid ' + hex(queue+stickOffset+1) + ':1 htb rate '+ str(upstreamBandwidthCapacityUploadMbps) + 'mbit ceil ' + str(upstreamBandwidthCapacityUploadMbps) + 'mbit'
linuxTCcommands.append(command)
command = 'qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 ' + sqm
command = 'qdisc add dev ' + thisInterface + ' parent ' + hex(queue+stickOffset+1) + ':1 ' + sqm
linuxTCcommands.append(command)
# Default class - traffic gets passed through this limiter with lower priority if it enters the top HTB without a specific class.
# Technically, that should not even happen. So don't expect much if any traffic in this default class.
# Only 1/4 of defaultClassCapacity is guaranteed (to prevent hitting ceiling of upstream), for the most part it serves as an "up to" ceiling.
command = 'class add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':1 classid ' + hex(queue+1) + ':2 htb rate ' + str(round((upstreamBandwidthCapacityUploadMbps-1)/4)) + 'mbit ceil ' + str(upstreamBandwidthCapacityUploadMbps-1) + 'mbit prio 5'
# Only 1/4 of defaultClassCapacity is guarenteed (to prevent hitting ceiling of upstream), for the most part it serves as an "up to" ceiling.
command = 'class add dev ' + thisInterface + ' parent ' + hex(queue+stickOffset+1) + ':1 classid ' + hex(queue+stickOffset+1) + ':2 htb rate ' + str(round((upstreamBandwidthCapacityUploadMbps-1)/4)) + 'mbit ceil ' + str(upstreamBandwidthCapacityUploadMbps-1) + 'mbit prio 5'
linuxTCcommands.append(command)
command = 'qdisc add dev ' + thisInterface + ' parent ' + hex(queue+1) + ':2 ' + sqm
command = 'qdisc add dev ' + thisInterface + ' parent ' + hex(queue+stickOffset+1) + ':2 ' + sqm
linuxTCcommands.append(command)
@@ -665,7 +685,9 @@ def refreshShapers():
for node in data:
command = 'class add dev ' + interfaceA + ' parent ' + data[node]['parentClassID'] + ' classid ' + data[node]['classMinor'] + ' htb rate '+ str(data[node]['downloadBandwidthMbpsMin']) + 'mbit ceil '+ str(data[node]['downloadBandwidthMbps']) + 'mbit prio 3'
linuxTCcommands.append(command)
command = 'class add dev ' + interfaceB + ' parent ' + data[node]['parentClassID'] + ' classid ' + data[node]['classMinor'] + ' htb rate '+ str(data[node]['uploadBandwidthMbpsMin']) + 'mbit ceil '+ str(data[node]['uploadBandwidthMbps']) + 'mbit prio 3'
print("Up ParentClassID: " + data[node]['up_parentClassID'])
print("ClassMinor: " + data[node]['classMinor'])
command = 'class add dev ' + interfaceB + ' parent ' + data[node]['up_parentClassID'] + ' classid ' + data[node]['classMinor'] + ' htb rate '+ str(data[node]['uploadBandwidthMbpsMin']) + 'mbit ceil '+ str(data[node]['uploadBandwidthMbps']) + 'mbit prio 3'
linuxTCcommands.append(command)
if 'circuits' in data[node]:
for circuit in data[node]['circuits']:
@@ -682,19 +704,24 @@ def refreshShapers():
if monitorOnlyMode == False:
command = 'qdisc add dev ' + interfaceA + ' parent ' + circuit['classMajor'] + ':' + circuit['classMinor'] + ' ' + sqm
linuxTCcommands.append(command)
command = 'class add dev ' + interfaceB + ' parent ' + data[node]['classid'] + ' classid ' + circuit['classMinor'] + ' htb rate '+ str(circuit['minUpload']) + 'mbit ceil '+ str(circuit['maxUpload']) + 'mbit prio 3'
command = 'class add dev ' + interfaceB + ' parent ' + data[node]['up_classid'] + ' classid ' + circuit['classMinor'] + ' htb rate '+ str(circuit['minUpload']) + 'mbit ceil '+ str(circuit['maxUpload']) + 'mbit prio 3'
linuxTCcommands.append(command)
# Only add CAKE / fq_codel qdisc if monitorOnlyMode is Off
if monitorOnlyMode == False:
command = 'qdisc add dev ' + interfaceB + ' parent ' + circuit['classMajor'] + ':' + circuit['classMinor'] + ' ' + sqm
command = 'qdisc add dev ' + interfaceB + ' parent ' + circuit['up_classMajor'] + ':' + circuit['classMinor'] + ' ' + sqm
linuxTCcommands.append(command)
pass
for device in circuit['devices']:
if device['ipv4s']:
for ipv4 in device['ipv4s']:
xdpCPUmapCommands.append('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --add --ip ' + str(ipv4) + ' --cpu ' + data[node]['cpuNum'] + ' --classid ' + circuit['classid'])
xdpCPUmapCommands.append('./bin/xdp_iphash_to_cpu_cmdline add --ip ' + str(ipv4) + ' --cpu ' + data[node]['cpuNum'] + ' --classid ' + circuit['classid'])
if OnAStick:
xdpCPUmapCommands.append('./bin/xdp_iphash_to_cpu_cmdline add --ip ' + str(ipv4) + ' --cpu ' + data[node]['up_cpuNum'] + ' --classid ' + circuit['up_classid'] + ' --upload 1')
if device['ipv6s']:
for ipv6 in device['ipv6s']:
xdpCPUmapCommands.append('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --add --ip ' + str(ipv6) + ' --cpu ' + data[node]['cpuNum'] + ' --classid ' + circuit['classid'])
xdpCPUmapCommands.append('./bin/xdp_iphash_to_cpu_cmdline add --ip ' + str(ipv6) + ' --cpu ' + data[node]['cpuNum'] + ' --classid ' + circuit['classid'])
if OnAStick:
xdpCPUmapCommands.append('./bin/xdp_iphash_to_cpu_cmdline add --ip ' + str(ipv6) + ' --cpu ' + data[node]['up_cpuNum'] + ' --classid ' + circuit['up_classid'] + ' --upload 1')
if device['deviceName'] not in devicesShaped:
devicesShaped.append(device['deviceName'])
# Recursive call this function for children nodes attached to this node
@@ -724,15 +751,17 @@ def refreshShapers():
xdpStartTime = datetime.now()
if enableActualShellCommands:
# Here we use os.system for the command, because otherwise it sometimes gltiches out with Popen in shell()
result = os.system('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --clear')
#result = os.system('./bin/xdp_iphash_to_cpu_cmdline clear')
clear_ip_mappings() # Use the bus
# Set up XDP-CPUMAP-TC
logging.info("# XDP Setup")
shell('./cpumap-pping/bin/xps_setup.sh -d ' + interfaceA + ' --default --disable')
shell('./cpumap-pping/bin/xps_setup.sh -d ' + interfaceB + ' --default --disable')
shell('./cpumap-pping/src/xdp_iphash_to_cpu --dev ' + interfaceA + ' --lan')
shell('./cpumap-pping/src/xdp_iphash_to_cpu --dev ' + interfaceB + ' --wan')
shell('./cpumap-pping/src/tc_classify --dev-egress ' + interfaceA)
shell('./cpumap-pping/src/tc_classify --dev-egress ' + interfaceB)
# Commented out - the daemon does this
#shell('./cpumap-pping/bin/xps_setup.sh -d ' + interfaceA + ' --default --disable')
#shell('./cpumap-pping/bin/xps_setup.sh -d ' + interfaceB + ' --default --disable')
#shell('./cpumap-pping/src/xdp_iphash_to_cpu --dev ' + interfaceA + ' --lan')
#shell('./cpumap-pping/src/xdp_iphash_to_cpu --dev ' + interfaceB + ' --wan')
#shell('./cpumap-pping/src/tc_classify --dev-egress ' + interfaceA)
#shell('./cpumap-pping/src/tc_classify --dev-egress ' + interfaceB)
xdpEndTime = datetime.now()
@@ -768,6 +797,7 @@ def refreshShapers():
for command in xdpCPUmapCommands:
logging.info(command)
print("Executed " + str(len(xdpCPUmapCommands)) + " XDP-CPUMAP-TC IP filter commands")
#print(xdpCPUmapCommands)
xdpFilterEndTime = datetime.now()
@@ -879,17 +909,20 @@ def refreshShapersUpdateOnly():
def removeDeviceIPsFromFilter(circuit):
for device in circuit['devices']:
for ipv4 in device['ipv4s']:
shell('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --del --ip ' + str(ipv4))
#shell('./bin/xdp_iphash_to_cpu_cmdline del ip ' + str(ipv4))
delete_ip_mapping(str(ipv4))
for ipv6 in device['ipv6s']:
shell('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --del --ip ' + str(ipv6))
#shell('./bin/xdp_iphash_to_cpu_cmdline del ip ' + str(ipv6))
delete_ip_mapping(str(ipv6))
def addDeviceIPsToFilter(circuit, cpuNumHex):
# TODO: Possible issue, check that the lqosd system expects the CPU in hex
for device in circuit['devices']:
for ipv4 in device['ipv4s']:
shell('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --add --ip ' + str(ipv4) + ' --cpu ' + cpuNumHex + ' --classid ' + circuit['classid'])
shell('./bin/xdp_iphash_to_cpu_cmdline add --ip ' + str(ipv4) + ' --cpu ' + cpuNumHex + ' --classid ' + circuit['classid'])
for ipv6 in device['ipv6s']:
shell('./cpumap-pping/src/xdp_iphash_to_cpu_cmdline --add --ip ' + str(ipv6) + ' --cpu ' + cpuNumHex + ' --classid ' + circuit['classid'])
shell('./bin/xdp_iphash_to_cpu_cmdline add --ip ' + str(ipv6) + ' --cpu ' + cpuNumHex + ' --classid ' + circuit['classid'])
def getAllParentNodes(data, allParentNodes):
@@ -1185,6 +1218,12 @@ def refreshShapersUpdateOnly():
print("refreshShapersUpdateOnly completed on " + datetime.now().strftime("%d/%m/%Y %H:%M:%S"))
if __name__ == '__main__':
if is_lqosd_alive:
print("lqosd is running")
else:
print("ERROR: lqosd is not running. Aborting")
os.exit()
parser = argparse.ArgumentParser()
parser.add_argument(
'-d', '--debug',

117
src/TESTING-1.4.md Normal file
View File

@@ -0,0 +1,117 @@
# How to Test V1.4
Version 1.4 is still undergoing active development, but if you'd like to benefit from it right now (or help us test/develop it!), here's a guide.
## Clone the repo
> My preferred install location is `/opt/libreqos` - but you can put it wherever you want.
Go to your preferred install location, and clone the repo:
```
git clone https://github.com/LibreQoE/LibreQoS.git
```
Switch to the development branch:
```
git checkout v1.4-pre-alpha-rust-integration
```
## Install Dependencies from apt and pip
You need to have a few packages from `apt` installed:
```
apt-get install -y python3-pip clang gcc gcc-multilib llvm libelf-dev git nano graphviz curl screen llvm pkg-config linux-tools-common linux-tools-`uname r` libbpf-dev
```
Then you need to install some Python dependencies:
```
python3 -m pip install ipaddress schedule influxdb-client requests flask flask_restful flask_httpauth waitress psutil binpacking graphviz
```
## Install the Rust development system
Go to [RustUp](https://rustup.rs) and follow the instructions. Basically, run the following:
```
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
When Rust finishes installing, it will tell you to execute a command to place the Rust build tools into your path. You need to either execute this command or logout and back in again.
Once that's done, change directory to `/wherever_you_put_libreqos/src/`, and run:
```
./build_rust.sh
```
This will take a while the first time, but it puts everything in the right place.
## Setup the LibreQoS Daemon
Copy the daemon configuration file to `/etc`:
```
sudo cp lqos.example /etc/lqos
```
Now edit the file to match your setup:
```toml
lqos_directory = '/opt/libreqos/src'
queue_check_period_ms = 1000
[tuning]
stop_irq_balance = true
netdev_budget_usecs = 8000
netdev_budget_packets = 300
rx_usecs = 8
tx_usecs = 8
disable_rxvlan = true
disable_txvlan = true
disable_offload = [ "gso", "tso", "lro", "sg", "gro" ]
interface_mapping = [
{ name = "enp1s0f1", redirect_to = "enp1s0f2", scan_vlans = false },
{ name = "enp1s0f2", redirect_to = "enp1s0f1", scan_vlans = false }
]
vlan_mapping = []
```
Change `enp1s0f1` and `enp1s0f2` to match your network interfaces. It doesn't matter which one is which.
## Configure LibreQoS
Follow the regular instructions to set your interfaces in `ispConfig.py` and your `network.json` and `ShapedDevices.csv` files.
## Run the program
You can setup `lqosd` and `lqos_node_manager` as daemons to keep running (there are example `systemd` files in the `src/bin` folder). Since v1.4 is under such heavy development, I recommend using `screen` to run detached instances - and make finding issues easier.
1. `screen`
2. `cd /wherever_you_put_libreqos/src/bin`
3. `sudo ./lqosd`
4. Create a new `screen` window with `Ctrl-A, C`.
5. Run the webserver with `./lqos_node_manager`
6. If you didn't see errors, detach with `Ctrl-A, D`
You can now point a web browser at `http://a.b.c.d:9123` (replace `a.b.c.d` with the management IP address of your shaping server) and enjoy a real-time view of your network.
In the web browser, click `Reload LibreQoS` to setup your shaping rules.
# Updating 1.4 Once You Have It
1. Resume screen with `screen -r`
2. Go to console 0 (`Ctrl-A, 0`) and stop `lqosd` with `ctrl+c`.
3. Go to console 1 (`Ctl-A, 1`) and stop `lqos_node_manager` with `ctrl+c`.
4. Detach from `screen` with `Ctrl-A, D`.
5. Change to your `LibreQoS` directory (e.g. `cd /opt/LibreQoS`)
6. Update from Git: `git pull`
7. Recompile: `./build-rust.sh`
8. Resume screen with `screen -r`.
9. Go to console 0 (`Ctrl-A, 0`) and run `sudo ./lqosd` to restart the bridge/manager.
10. Go to console 1 (`Ctrl-A, 1`) and run `./lqos_node_manager` to restart the web server.
11. If you didn't see errors, detach with `Ctrl-A, D`

View File

@@ -0,0 +1,10 @@
[Unit]
After=network.service
[Service]
WorkingDirectory=/opt/libreqos/src/bin
ExecStart=/opt/libreqos/src/bin/lqos_node_manager
Restart=always
[Install]
WantedBy=default.target

View File

@@ -0,0 +1,10 @@
[Unit]
After=network.service
[Service]
WorkingDirectory=/opt/libreqos/src/bin
ExecStart=/opt/libreqos/src/bin/lqosd
Restart=always
[Install]
WantedBy=default.target

40
src/build_rust.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# 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.
#
# Don't forget to setup `/etc/lqos`
PROGS="lqosd lqtop xdp_iphash_to_cpu_cmdline xdp_pping lqos_node_manager webusers"
mkdir -p bin/static
pushd rust
#cargo clean
for prog in $PROGS
do
pushd $prog
cargo build --release
popd
done
for prog in $PROGS
do
cp target/release/$prog ../bin
done
popd
# Copy the node manager's static web content
cp -R rust/lqos_node_manager/static/* bin/static
# Copy Rocket.toml to tell the node manager where to listen
cp rust/lqos_node_manager/Rocket.toml bin/
# Copy the Python library for LibreQoS.py et al.
pushd rust/lqos_python
cargo build --release
popd
cp rust/target/release/liblqos_python.so .
echo "Don't forget to setup /etc/lqos!"
echo "Template .service files can be found in bin/"

Submodule src/cpumap-pping deleted from 0d4df7f918

View File

@@ -306,7 +306,7 @@ def getParentNodeLatencyStats(parentNodes, subscriberCircuits):
def getCircuitLatencyStats(subscriberCircuits):
command = './cpumap-pping/src/xdp_pping'
command = './src/bin/xdp_pping'
listOfEntries = json.loads(subprocess.run(command.split(' '), stdout=subprocess.PIPE).stdout.decode('utf-8'))
tcpLatencyForClassID = {}

View File

@@ -23,6 +23,14 @@ interfaceA = 'eth1'
# Interface connected to edge router
interfaceB = 'eth2'
## WORK IN PROGRESS. Note that interfaceA determines the "stick" interface
## I could only get scanning to work if I issued ethtool -K enp1s0f1 rxvlan off
OnAStick = False
# VLAN facing the core router
StickVlanA = 0
# VLAN facing the edge router
StickVlanB = 0
# Allow shell commands. False causes commands print to console only without being executed.
# MUST BE ENABLED FOR PROGRAM TO FUNCTION
enableActualShellCommands = True

34
src/lqos.example Normal file
View File

@@ -0,0 +1,34 @@
# This file *must* be installed in `/etc/lqos`.
# Change the values to match your setup.
# Where is LibreQoS installed?
lqos_directory = '/opt/libreqos/src'
queue_check_period_ms = 1000
[tuning]
stop_irq_balance = true
netdev_budget_usecs = 8000
netdev_budget_packets = 300
rx_usecs = 8
tx_usecs = 8
disable_rxvlan = true
disable_txvlan = true
disable_offload = [ "gso", "tso", "lro", "sg", "gro" ]
# If you are running a traditional two-interface setup, use:
# interface_mapping = [
# { name = "enp1s0f1", redirect_to = "enp1s0f2", scan_vlans = false },
# { name = "enp1s0f2", redirect_to = "enp1s0f1", scan_vlans = false }
# ]
# vlan_mapping = []
# For "on a stick":
[bridge]
use_kernel_bridge = true
interface_mapping = [
{ name = "enp1s0f1", redirect_to = "enp1s0f1", scan_vlans = true }
]
vlan_mapping = [
{ parent = "enp1s0f1", tag = 3, redirect_to = 4 },
{ parent = "enp1s0f1", tag = 4, redirect_to = 3 }
]

2839
src/rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
src/rust/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "lqos_rs"
version = "0.1.0"
edition = "2021"
[dependencies]
[profile.release]
strip = "debuginfo"
lto = "thin"
[workspace]
members = [
"lqos_sys", # System support for handling the XDP component
"lqos_config", # Configuration support
"lqosd", # LibreQoS Daemon
"lqos_bus", # Bus data types
"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
"webusers", # CLI control for managing the web user list
]

32
src/rust/README.md Normal file
View File

@@ -0,0 +1,32 @@
# Rust Management System for LibreQoS
> Very much a work in progress. Details will be filled out as it stabilizes.
## Sub Projects
This project contains a number of projects arranged in a workspace. The projects are:
* `lqos_sys` - a library that builds, installs, removes and manages the LibreQoS XDP and TC programs.
* `lqos_bus` - definitions and helper functions for passing data across the local management bus.
* `lqos_config` - a crate that handles pulling configuration from the Python manager.
* `lqosd` - the management daemon that should eventually be run as a `systemd` service.
* When started, the daemon sets up XDP/TC eBPF programs for the interfaces specified in the LibreQoS configuration.
* When exiting, all eBPF programs are unloaded.
* Listens for bus commands and applies them.
* `lqtop` - A CLI tool that outputs the top X downloaders and mostly verifies that the bus and daemons work.
* `xdp_iphash_to_cpu_cmdline` - An almost-compatible command that acts like the tool of the same name from the previous verion.
* `xdp_pping` - Port of the previous release's `xdp_pping` tool, for compatibility. Will eventually not be needed.
## Required Ubuntu packages
* `clang`
* `linux-tools-common` (for `bpftool`)
* `libbpf-dev`
* `gcc-multilib`
* `llvm`
* `pkg-config`
* `linux-tools-5.15.0-56-generic` (the common version doesn't work?)
## Helper Scripts
* `remove_pinned_maps.sh` deletes all of the BPF shared maps. Useful during development.

View File

@@ -0,0 +1,17 @@
[package]
name = "lqos_bus"
version = "0.1.0"
edition = "2021"
[features]
default = ["equinix_tests"]
equinix_tests = []
[dependencies]
serde = { version = "1.0", features = ["derive"] }
bincode = "1"
anyhow = "1"
lqos_config = { path = "../lqos_config" }
[build-dependencies]
cc = "1.0"

View File

@@ -0,0 +1,5 @@
fn main() {
cc::Build::new()
.file("src/tc_handle_parser.c")
.compile("tc_handle_parse.o");
}

View File

@@ -0,0 +1,76 @@
mod session;
mod request;
mod reply;
mod response;
pub use session::BusSession;
pub use request::BusRequest;
pub use reply::BusReply;
pub use response::BusResponse;
use anyhow::Result;
/// The address to which `lqosd` should bind itself when listening for
/// local bust requests.
///
/// This is typically `localhost` to minimize the exposed footprint.
pub const BUS_BIND_ADDRESS: &str = "127.0.0.1:9999";
/// Encodes a BusSession with `bincode`, providing a tight binary
/// representation of the request object for TCP transmission.
pub fn encode_request(request: &BusSession) -> Result<Vec<u8>> {
Ok(bincode::serialize(request)?)
}
/// Decodes bytes into a `BusSession`.
pub fn decode_request(bytes: &[u8]) -> Result<BusSession> {
Ok(bincode::deserialize(&bytes)?)
}
/// Encodes a `BusReply` object with `bincode`.
pub fn encode_response(request: &BusReply) -> Result<Vec<u8>> {
Ok(bincode::serialize(request)?)
}
/// Decodes a `BusReply` object with `bincode`.
pub fn decode_response(bytes: &[u8]) -> Result<BusReply> {
Ok(bincode::deserialize(&bytes)?)
}
/// The cookie value to use to determine that the session is valid.
pub fn cookie_value() -> u32 {
1234
}
#[cfg(test)]
mod test {
use super::*;
use crate::{BusRequest, BusResponse};
#[test]
fn test_session_roundtrip() {
let session = BusSession {
auth_cookie: cookie_value(),
requests: vec![
BusRequest::Ping,
]
};
let bytes = encode_request(&session).unwrap();
let new_session = decode_request(&bytes).unwrap();
assert_eq!(new_session.auth_cookie, session.auth_cookie);
assert_eq!(new_session.requests.len(), session.requests.len());
assert_eq!(new_session.requests[0], session.requests[0]);
}
#[test]
fn test_reply_roundtrip() {
let reply = BusReply {
auth_cookie: cookie_value(),
responses: vec![BusResponse::Ack]
};
let bytes = encode_response(&reply).unwrap();
let new_reply = decode_response(&bytes).unwrap();
assert_eq!(reply.auth_cookie, new_reply.auth_cookie);
assert_eq!(reply.responses.len(), new_reply.responses.len());
assert_eq!(reply.responses[0], new_reply.responses[0]);
}
}

View File

@@ -0,0 +1,20 @@
use serde::{Serialize, Deserialize};
use crate::BusResponse;
/// A single reply, always generated in response to a `BusSession` request.
/// Echoes the `auth_cookie` back to ensure that connectivity is valid,
/// and contains one or more `BusResponse` objects with the details
/// of the reply to each request.
///
/// No ordering guarantee is present. Responses may be out-of-order with
/// respect to the order of the requests.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BusReply {
/// Auth cookie, which should match the output of the `auth_cookie`
/// function.
pub auth_cookie: u32,
/// A list of `BusResponse` objects generated in response to the
/// requests that started the session.
pub responses: Vec<BusResponse>,
}

View File

@@ -0,0 +1,94 @@
use lqos_config::Tunables;
use serde::{Serialize, Deserialize};
use crate::TcHandle;
/// One or more `BusRequest` objects must be included in a `BusSession`
/// request. Each `BusRequest` represents a single request for action
/// or data.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum BusRequest {
/// A generic "is it alive?" test. Returns an `Ack`.
Ping,
/// Request total current throughput. Returns a
/// `BusResponse::CurrentThroughput` value.
GetCurrentThroughput,
/// Retrieve the top N downloads by bandwidth use.
GetTopNDownloaders(u32),
/// Retrieves the TopN hosts with the worst RTT, sorted by RTT descending.
GetWorstRtt(u32),
/// Retrieves current byte counters for all hosts.
GetHostCounter,
/// Requests that the XDP back-end associate an IP address with a
/// TC (traffic control) handle, and CPU. The "upload" flag indicates
/// that this is a second channel applied to the SAME network interface,
/// used for "on-a-stick" mode upload channels.
MapIpToFlow {
/// The IP address to map, as a string. It can be IPv4 or IPv6,
/// and supports CIDR notation for subnets. "192.168.1.1",
/// "192.168.1.0/24", are both valid.
ip_address: String,
/// The TC Handle to which the IP address should be mapped.
tc_handle: TcHandle,
/// The CPU on which the TC handle should be shaped.
cpu: u32,
/// If true, this is a *second* flow for the same IP range on
/// the same NIC. Used for handling "on a stick" configurations.
upload: bool,
},
/// Requests that the XDP program unmap an IP address/subnet from
/// the traffic management system.
DelIpFlow {
/// The IP address to unmap. It can be an IPv4, IPv6 or CIDR
/// subnet.
ip_address: String,
/// Should we delete a secondary mapping (for upload)?
upload: bool,
},
/// Clear all XDP IP/TC/CPU mappings.
ClearIpFlow,
/// Retreieve list of all current IP/TC/CPU mappings.
ListIpFlow,
/// Simulate the previous version's `xdp_pping` command, returning
/// RTT data for all mapped flows by TC handle.
XdpPping,
/// Divide current RTT data into histograms and return the data for
/// rendering.
RttHistogram,
/// Cound the number of mapped and unmapped hosts detected by the
/// system.
HostCounts,
/// Retrieve a list of all unmapped IPs that have been detected
/// carrying traffic.
AllUnknownIps,
/// Reload the `LibreQoS.py` program and return details of the
/// reload run.
ReloadLibreQoS,
/// Retrieve raw queue data for a given circuit ID.
GetRawQueueData(String), // The string is the circuit ID
/// Requests a real-time adjustment of the `lqosd` tuning settings
UpdateLqosDTuning(u64, Tunables),
/// If running on Equinix (the `equinix_test` feature is enabled),
/// display a "run bandwidht test" link.
#[cfg(feature = "equinix_tests")]
RequestLqosEquinixTest,
}

View File

@@ -0,0 +1,61 @@
use std::net::IpAddr;
use serde::{Serialize, Deserialize};
use crate::{IpStats, IpMapping, XdpPpingResult};
/// A `BusResponse` object represents a single
/// reply generated from a `BusRequest`, and batched
/// inside a `BusReply`.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum BusResponse {
/// Yes, we're alive
Ack,
/// An operation failed, with the enclosed error message.
Fail(String),
/// Current throughput for the overall system.
CurrentThroughput {
/// In bps
bits_per_second: (u64, u64),
/// In pps
packets_per_second: (u64, u64),
/// How much of the response has been subject to the shaper?
shaped_bits_per_second: (u64, u64),
},
/// Provides a list of ALL mapped hosts traffic counters,
/// listing the IP Address and upload/download in a tuple.
HostCounters(Vec<(IpAddr, u64, u64)>),
/// Provides the Top N downloaders IP stats.
TopDownloaders(Vec<IpStats>),
/// Provides the worst N RTT scores, sorted in descending order.
WorstRtt(Vec<IpStats>),
/// List all IP/TC mappings.
MappedIps(Vec<IpMapping>),
/// Return the data required for compatability with the `xdp_pping`
/// program.
XdpPping(Vec<XdpPpingResult>),
/// Return the data required to render the RTT histogram on the
/// local web GUI.
RttHistogram(Vec<u32>),
/// A tuple of (mapped)(unknown) host counts.
HostCounts((u32, u32)),
/// A list of all unmapped IP addresses that have been detected.
AllUnknownIps(Vec<IpStats>),
/// The results of reloading LibreQoS.
ReloadLibreQoS(String),
/// A string containing a JSON dump of a queue stats. Analagos to
/// the response from `tc show qdisc`.
RawQueueData(String),
}

View File

@@ -0,0 +1,16 @@
use serde::{Serialize, Deserialize};
use crate::BusRequest;
/// `BusSession` represents a complete session with `lqosd`. It must
/// contain a cookie value (defined in the `cookie_value()` function),
/// which serves as a sanity check that the connection is valid.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BusSession {
/// Authentication cookie that must match the `auth_cookie()` function's
/// return value.
pub auth_cookie: u32,
/// A list of requests to include in this session.
pub requests: Vec<BusRequest>,
}

View File

@@ -0,0 +1,65 @@
use crate::TcHandle;
use serde::{Deserialize, Serialize};
/// Transmission representation of IP statistics associated
/// with a host.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct IpStats {
/// The host's IP address, as detected by the XDP program.
pub ip_address: String,
/// The current bits-per-second passing through this host. Tuple
/// 0 is download, tuple 1 is upload.
pub bits_per_second: (u64, u64),
/// The current packets-per-second passing through this host. Tuple
/// 0 is download, tuple 1 is upload.
pub packets_per_second: (u64, u64),
/// Median TCP round-trip-time for this host at the current time.
pub median_tcp_rtt: f32,
/// Associated TC traffic control handle.
pub tc_handle: TcHandle,
}
/// Represents an IP Mapping in the XDP IP to TC/CPU mapping system.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct IpMapping {
/// The mapped IP address. May be IPv4, or IPv6.
pub ip_address: String,
/// The CIDR prefix length of the host. Equivalent to the CIDR value
/// after the /. e.g. `/24`.
pub prefix_length: u32,
/// The current TC traffic control handle.
pub tc_handle: TcHandle,
/// The CPU index associated with this IP mapping.
pub cpu: u32,
}
/// Provided for backwards compatibility with `xdp_pping`, with the intent
/// to retire it eventually.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct XdpPpingResult {
/// The TC handle in text format. e.g. "1:12"
pub tc: String,
/// The average (mean) RTT value for the current sample.
pub avg: f32,
/// The minimum RTT value for the current sample.
pub min: f32,
/// The maximum RTT value for the current sample.
pub max: f32,
/// The median RTT value for the current sample.
pub median: f32,
/// The number of samples from which these values were
/// derived. If 0, the other values are invalid.
pub samples: u32,
}

View File

@@ -0,0 +1,21 @@
//! The `lqos_bus` crate provides the data-transfer back-end for communication
//! between the various parts of LibreQoS. `lqosd` listens on `localhost`
//! for requests. Any tool may use the daemon services locally for interaction
//! with the LibreQoS system.
//!
//! A normal session consists of connecting and sending a single `BusSession`
//! object (serialized with `bincode`), that must contain one or more
//! `BusRequest` objects. Replies are then batched inside a `BusReply`
//! object, containing one or more `BusResponse` detail objects.
//! The session then terminates.
#![warn(missing_docs)]
mod bus;
mod ip_stats;
pub use ip_stats::{IpMapping, IpStats, XdpPpingResult};
mod tc_handle;
pub use tc_handle::TcHandle;
pub use bus::{BUS_BIND_ADDRESS, BusSession, BusRequest, BusReply,
BusResponse, encode_request, decode_request, encode_response,
decode_response, cookie_value};

View File

@@ -0,0 +1,133 @@
use anyhow::{Error, Result};
use serde::{Deserialize, Serialize};
use std::ffi::CString;
/// Provides consistent handling of TC handle types.
#[derive(Copy, Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
pub struct TcHandle(u32);
#[allow(non_camel_case_types)]
type __u32 = ::std::os::raw::c_uint;
#[allow(dead_code)]
const TC_H_ROOT: u32 = 4294967295;
#[allow(dead_code)]
const TC_H_UNSPEC: u32 = 0;
extern "C" {
pub fn get_tc_classid(
h: *mut __u32,
str_: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
impl TcHandle {
/// Returns the TC handle as two values, indicating major and minor
/// TC handle values.
#[inline(always)]
pub fn get_major_minor(&self) -> (u16, u16) {
// According to xdp_pping.c handles are minor:major u16s inside
// a u32.
((self.0 >> 16) as u16, (self.0 & 0xFFFF) as u16)
}
/// Build a TC handle from a string. This is actually a complicated
/// operation, since it has to handle "root" and other strings as well
/// as simple "1:2" mappings. Shells out to C to handle this gracefully.
pub fn from_string<S: ToString>(handle: S) -> Result<Self> {
let mut tc_handle: __u32 = 0;
let str = CString::new(handle.to_string())?;
let handle_pointer: *mut __u32 = &mut tc_handle;
let result = unsafe { get_tc_classid(handle_pointer, str.as_ptr()) };
if result != 0 {
Err(Error::msg("Unable to parse TC handle string"))
} else {
Ok(Self(tc_handle))
}
}
/// Construct a TC handle from a raw 32-bit unsigned integer.
pub fn from_u32(tc: u32) -> Self {
Self(tc)
}
/// Retreives a TC handle as a raw 32-bit unsigned integer.
pub fn as_u32(&self) -> u32 {
self.0
}
/// Construct a zeroed TC handle.
pub fn zero() -> Self {
Self(0)
}
}
impl ToString for TcHandle {
fn to_string(&self) -> String {
let (major, minor) = self.get_major_minor();
format!("{major:x}:{minor:x}")
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn make_root() {
let tc = TcHandle::from_string("root").unwrap();
assert_eq!(tc.0, TC_H_ROOT);
}
#[test]
fn make_unspecified() {
let tc = TcHandle::from_string("none").unwrap();
assert_eq!(tc.0, TC_H_UNSPEC);
}
#[test]
fn test_invalid() {
let tc = TcHandle::from_string("not_a_number");
assert!(tc.is_err());
}
#[test]
fn oversize_major() {
let tc = TcHandle::from_string("65540:0");
assert!(tc.is_err());
}
#[test]
fn oversize_minor() {
let tc = TcHandle::from_string("0:65540");
assert!(tc.is_err());
}
#[test]
fn zero() {
let tc = TcHandle::from_string("0:0").unwrap();
assert_eq!(tc.0, 0);
}
#[test]
fn roundtrip() {
let tc = TcHandle::from_string("1:2").unwrap();
assert_eq!(tc.to_string(), "1:2");
}
#[test]
fn hex() {
let tc = TcHandle::from_string("7FFF:2").unwrap();
assert_eq!(tc.to_string().to_uppercase(), "7FFF:2");
}
#[test]
fn roundtrip_extreme() {
for major in 0..2000 {
for minor in 0..2000 {
let handle = format!("{major:x}:{minor:x}");
let tc = TcHandle::from_string(&handle).unwrap();
assert_eq!(tc.to_string(), handle);
}
}
}
}

View File

@@ -0,0 +1,46 @@
// Imported from https://github.com/thebracket/cpumap-pping/blob/master/src/xdp_iphash_to_cpu_cmdline.c
// Because it uses strtoul and is based on the TC source, including it directly
// seemed like the path of least resistance.
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <linux/types.h>
#include <linux/pkt_sched.h> /* TC macros */
/* Handle classid parsing based on iproute source */
int get_tc_classid(__u32 *h, const char *str)
{
__u32 major, minor;
char *p;
major = TC_H_ROOT;
if (strcmp(str, "root") == 0)
goto ok;
major = TC_H_UNSPEC;
if (strcmp(str, "none") == 0)
goto ok;
major = strtoul(str, &p, 16);
if (p == str) {
major = 0;
if (*p != ':')
return -1;
}
if (*p == ':') {
if (major >= (1<<16))
return -1;
major <<= 16;
str = p+1;
minor = strtoul(str, &p, 16);
if (*p != 0)
return -1;
if (minor >= (1<<16))
return -1;
major |= minor;
} else if (*p != 0)
return -1;
ok:
*h = major;
return 0;
}

View File

@@ -0,0 +1,14 @@
[package]
name = "lqos_config"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
toml = "0.5"
serde = { version = "1.0", features = [ "derive" ] }
csv = "1"
ip_network_table = "0"
ip_network = "0"
sha2 = "0"
uuid = { version = "1", features = ["v4", "fast-rng" ] }

View File

@@ -0,0 +1,15 @@
# LQosConfig
`lqos_config` is designed to manage configuration of LibreQoS.
Since all of the parts of the system need to know where to find LibreQoS, it first looks for a file named `/etc/lqos` and uses that to locate the LibreQoS installation.
`/etc/lqos` looks like this:
```toml
lqos_directory = '/opt/libreqos'
```
The entries are:
* `lqos_directory`: where LibreQoS is installed (e.g. `/opt/libreqos`)

View File

@@ -0,0 +1,220 @@
//! The `authentication` module provides authorization for use of the
//! local web UI on LibreQoS boxes. It maps to `/<install dir>/webusers.toml`
use anyhow::{Error, Result};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
fmt::Display,
fs::{read_to_string, remove_file, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
use uuid::Uuid;
/// Access rights of a user
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum UserRole {
/// The user may view data but not change it.
ReadOnly,
/// The user may make any changes they request.
Admin,
}
impl From<&str> for UserRole {
fn from(s: &str) -> Self {
let s = s.to_lowercase();
if s == "admin" {
UserRole::Admin
} else {
UserRole::ReadOnly
}
}
}
impl Display for UserRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserRole::Admin => write!(f, "admin"),
UserRole::ReadOnly => write!(f, "read-only"),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct WebUser {
username: String,
password_hash: String,
role: UserRole,
token: String,
}
/// Container holding the authorized web users.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct WebUsers {
allow_unauthenticated_to_view: bool,
users: Vec<WebUser>,
}
impl Default for WebUsers {
fn default() -> Self {
Self {
users: Vec::new(),
allow_unauthenticated_to_view: false,
}
}
}
impl WebUsers {
fn path() -> Result<PathBuf> {
let base_path = crate::EtcLqos::load()?.lqos_directory;
let filename = Path::new(&base_path).join("webusers.toml");
Ok(filename)
}
fn save_to_disk(&self) -> Result<()> {
let path = Self::path()?;
let new_contents = toml::to_string(&self)?;
if path.exists() {
remove_file(&path)?;
}
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
file.write_all(&new_contents.as_bytes())?;
Ok(())
}
/// Does the user's file exist? True if it does, false otherwise.
pub fn does_users_file_exist() -> Result<bool> {
Ok(Self::path()?.exists())
}
/// Try to load `webusers.toml`. If it is unavailable, create a new--empty--
/// file.
pub fn load_or_create() -> Result<Self> {
let path = Self::path()?;
if !path.exists() {
// Create a new users file, save it and return the
// empty file
let new_users = Self::default();
new_users.save_to_disk()?;
Ok(new_users)
} else {
// Load from disk
let raw = read_to_string(path)?;
let users = toml::from_str(&raw)?;
Ok(users)
}
}
fn hash_password(password: &str) -> String {
let salted = format!("!x{password}_LibreQosLikesPasswordsForDinner");
let mut sha256 = Sha256::new();
sha256.update(salted);
format!("{:X}", sha256.finalize())
}
/// If a user exists with this username, update their details to the
/// provided values. If the user does not exist, create them with the
/// provided values.
pub fn add_or_update_user(
&mut self,
username: &str,
password: &str,
role: UserRole,
) -> Result<String> {
let token; // Assigned in a branch
if let Some(mut user) = self.users.iter_mut().find(|u| u.username == username) {
user.password_hash = Self::hash_password(password);
user.role = role;
token = user.token.clone();
} else {
token = Uuid::new_v4().to_string();
let new_user = WebUser {
username: username.to_string(),
password_hash: Self::hash_password(password),
role,
token: token.clone(),
};
self.users.push(new_user);
}
self.save_to_disk()?;
Ok(token)
}
/// Delete a user from `webusers.toml`
pub fn remove_user(&mut self, username: &str) -> Result<()> {
let old_len = self.users.len();
self.users.retain(|u| u.username != username);
if old_len == self.users.len() {
return Err(Error::msg(format!("User {} was not found", username)));
}
self.save_to_disk()?;
Ok(())
}
/// Attempt a login with the specified username and password. If
/// the login succeeds, returns the publically shareable token that
/// uniquely identifies the user a a string. If it fails, returns an
/// `Err`.
pub fn login(&self, username: &str, password: &str) -> Result<String> {
let hash = Self::hash_password(password);
if let Some(user) = self
.users
.iter()
.find(|u| u.username == username && u.password_hash == hash)
{
Ok(user.token.clone())
} else {
if self.allow_unauthenticated_to_view {
Ok("default".to_string())
} else {
Err(Error::msg("Invalid Login"))
}
}
}
/// Given a token, lookup the matching user and return their role.
pub fn get_role_from_token(&self, token: &str) -> Result<UserRole> {
if let Some(user) = self.users.iter().find(|u| u.token == token) {
Ok(user.role)
} else {
if self.allow_unauthenticated_to_view {
Ok(UserRole::ReadOnly)
} else {
Err(Error::msg("Unknown user token"))
}
}
}
/// Given a token, lookup the matching user and return their username.
pub fn get_username(&self, token: &str) -> String {
if let Some(user) = self.users.iter().find(|u| u.token == token) {
user.username.clone()
} else {
"Anonymous".to_string()
}
}
/// Dump all users to the console.
pub fn print_users(&self) -> Result<()> {
self.users.iter().for_each(|u| {
println!("{:<40} {:<10}", u.username, u.role.to_string());
});
Ok(())
}
/// Sets the "allow unauthenticated users" field. If true,
/// unauthenticated users gain read-only access. This is useful
/// for demonstration purposes.
pub fn allow_anonymous(&mut self, allow: bool) -> Result<()> {
self.allow_unauthenticated_to_view = allow;
self.save_to_disk()?;
Ok(())
}
/// Do we allow unauthenticated users to read site data?
pub fn do_we_allow_anonymous(&self) -> bool {
self.allow_unauthenticated_to_view
}
}

View File

@@ -0,0 +1,118 @@
//! Manages the `/etc/lqos` file.
use anyhow::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
/// Represents the top-level of the `/etc/lqos` file. Serialization
/// structure.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct EtcLqos {
/// The directory in which LibreQoS is installed.
pub lqos_directory: String,
/// How frequently should `lqosd` read the `tc show qdisc` data?
/// In ms.
pub queue_check_period_ms: u64,
/// If present, defines how the Bifrost XDP bridge operates.
pub bridge: Option<BridgeConfig>,
/// If present, defines the values for various `sysctl` and `ethtool`
/// tweaks.
pub tuning: Option<Tunables>,
}
/// Represents a set of `sysctl` and `ethtool` tweaks that may be
/// applied (in place of the previous version's offload service)
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct Tunables {
/// Should the `irq_balance` system service be stopped?
pub stop_irq_balance: bool,
/// Set the netdev budget (usecs)
pub netdev_budget_usecs: u32,
/// Set the netdev budget (packets)
pub netdev_budget_packets: u32,
/// Set the RX side polling frequency
pub rx_usecs: u32,
/// Set the TX side polling frequency
pub tx_usecs: u32,
/// Disable RXVLAN offloading? You generally want to do this.
pub disable_rxvlan: bool,
/// Disable TXVLAN offloading? You generally want to do this.
pub disable_txvlan: bool,
/// A list of `ethtool` offloads to be disabled.
/// The default list is: [ "gso", "tso", "lro", "sg", "gro" ]
pub disable_offload: Vec<String>,
}
/// Defines the BiFrost XDP bridge accelerator parameters
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BridgeConfig {
/// Should the XDP bridge be enabled?
pub use_kernel_bridge: bool,
/// A list of interface mappings.
pub interface_mapping: Vec<BridgeInterface>,
/// A list of VLAN mappings.
pub vlan_mapping: Vec<BridgeVlan>,
}
/// An interface within the Bifrost XDP bridge.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BridgeInterface {
/// The interface name. It *must* match an interface name
/// findable by Linux.
pub name: String,
/// Should Bifrost read VLAN tags and determine redirect
/// policy from there?
pub scan_vlans: bool,
/// The outbound interface - data that arrives in the interface
/// defined by `name` will be redirected to this interface.
///
/// If you are using an "on a stick" configuration, this will
/// be the same as `name`.
pub redirect_to: String,
}
/// If `scan_vlans` is enabled for an interface, then VLANs
/// are examined on the way through the XDP BiFrost bridge.
///
/// If a VLAN is on the `parent` interface, and matches `tag` - it
/// will be moved to VLAN `redirect_to`.
///
/// You often need to make reciprocal pairs of these.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BridgeVlan {
/// The parent interface name on which the VLAN occurs.
pub parent: String,
/// The VLAN tag number to redirect if matched.
pub tag: u32,
/// The destination VLAN tag number if matched.
pub redirect_to: u32,
}
impl EtcLqos {
/// Loads `/etc/lqos`.
pub fn load() -> Result<Self> {
if !Path::new("/etc/lqos").exists() {
return Err(Error::msg("You must setup /etc/lqos"));
}
let raw = std::fs::read_to_string("/etc/lqos")?;
let config: Self = toml::from_str(&raw)?;
//println!("{:?}", config);
Ok(config)
}
}

View File

@@ -0,0 +1,19 @@
//! The `lqos_config` crate stores and handles LibreQoS configuration.
//! Configuration is drawn from:
//! * The `ispConfig.py` file.
//! * The `/etc/lqos` file.
//! * `ShapedDevices.csv` files.
//! * `network.json` files.
#![warn(missing_docs)]
mod authentication;
mod etc;
mod libre_qos_config;
mod program_control;
mod shaped_devices;
pub use authentication::{UserRole, WebUsers};
pub use etc::{BridgeConfig, BridgeInterface, BridgeVlan, EtcLqos, Tunables};
pub use libre_qos_config::LibreQoSConfig;
pub use program_control::load_libreqos;
pub use shaped_devices::{ConfigShapedDevices, ShapedDevice};

View File

@@ -0,0 +1,287 @@
//! `ispConfig.py` is part of the Python side of LibreQoS. This module
//! reads, writes and maps values from the Python file.
use crate::etc;
use anyhow::{Error, Result};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, read_to_string, remove_file, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
/// Represents the contents of an `ispConfig.py` file.
#[derive(Serialize, Deserialize, Debug)]
pub struct LibreQoSConfig {
/// Interface facing the Internet
pub internet_interface: String,
/// Interface facing the ISP Core Router
pub isp_interface: String,
/// Are we in "on a stick" (single interface) mode?
pub on_a_stick_mode: bool,
/// If we are, which VLAN represents which direction?
/// In (internet, ISP) order.
pub stick_vlans: (u16, u16),
/// The value of the SQM field from `ispConfig.py`
pub sqm: String,
/// Are we in monitor-only mode (not shaping)?
pub monitor_mode: bool,
/// Total available download (in Mbps)
pub total_download_mbps: u32,
/// Total available upload (in Mbps)
pub total_upload_mbps: u32,
/// If a node is generated, how much download (Mbps) should it offer?
pub generated_download_mbps: u32,
/// If a node is generated, how much upload (Mbps) should it offer?
pub generated_upload_mbps: u32,
/// Should the Python queue builder use the bin packing strategy to
/// try to optimize CPU assignment?
pub use_binpacking: bool,
/// Should the Python program use actual shell commands (and execute)
/// them?
pub enable_shell_commands: bool,
/// Should every issued command be prefixed with `sudo`?
pub run_as_sudo: bool,
/// WARNING: generally don't touch this.
pub override_queue_count: u32,
}
impl LibreQoSConfig {
/// Loads `ispConfig.py` into a management object.
pub fn load() -> Result<Self> {
let cfg = etc::EtcLqos::load()?;
let base_path = Path::new(&cfg.lqos_directory);
let final_path = base_path.join("ispConfig.py");
Ok(Self::load_from_path(&final_path)?)
}
fn load_from_path(path: &PathBuf) -> Result<Self> {
let path = Path::new(path);
if !path.exists() {
return Err(Error::msg("Unable to find ispConfig.py"));
}
// Read the config
let mut result = Self {
internet_interface: String::new(),
isp_interface: String::new(),
on_a_stick_mode: false,
stick_vlans: (0, 0),
sqm: String::new(),
monitor_mode: false,
total_download_mbps: 0,
total_upload_mbps: 0,
generated_download_mbps: 0,
generated_upload_mbps: 0,
use_binpacking: false,
enable_shell_commands: true,
run_as_sudo: false,
override_queue_count: 0,
};
result.parse_isp_config(path)?;
Ok(result)
}
fn parse_isp_config(&mut self, path: &Path) -> Result<()> {
let content = fs::read_to_string(path)?;
for line in content.split("\n") {
if line.starts_with("interfaceA") {
self.isp_interface = split_at_equals(line);
}
if line.starts_with("interfaceB") {
self.internet_interface = split_at_equals(line);
}
if line.starts_with("OnAStick") {
let mode = split_at_equals(line);
if mode == "True" {
self.on_a_stick_mode = true;
}
}
if line.starts_with("StickVlanA") {
let vlan_string = split_at_equals(line);
let vlan: u16 = vlan_string.parse()?;
self.stick_vlans.0 = vlan;
}
if line.starts_with("StickVlanB") {
let vlan_string = split_at_equals(line);
let vlan: u16 = vlan_string.parse()?;
self.stick_vlans.1 = vlan;
}
if line.starts_with("sqm") {
self.sqm = split_at_equals(line);
}
if line.starts_with("upstreamBandwidthCapacityDownloadMbps") {
self.total_download_mbps = split_at_equals(line).parse()?;
}
if line.starts_with("upstreamBandwidthCapacityUploadMbps") {
self.total_upload_mbps = split_at_equals(line).parse()?;
}
if line.starts_with("monitorOnlyMode ") {
let mode = split_at_equals(line);
if mode == "True" {
self.monitor_mode = true;
}
}
if line.starts_with("generatedPNDownloadMbps") {
self.generated_download_mbps = split_at_equals(line).parse()?;
}
if line.starts_with("generatedPNUploadMbps") {
self.generated_upload_mbps = split_at_equals(line).parse()?;
}
if line.starts_with("useBinPackingToBalanceCPU") {
let mode = split_at_equals(line);
if mode == "True" {
self.use_binpacking = true;
}
}
if line.starts_with("enableActualShellCommands") {
let mode = split_at_equals(line);
if mode == "True" {
self.enable_shell_commands = true;
}
}
if line.starts_with("runShellCommandsAsSudo") {
let mode = split_at_equals(line);
if mode == "True" {
self.run_as_sudo = true;
}
}
if line.starts_with("queuesAvailableOverride") {
self.override_queue_count = split_at_equals(line).parse().unwrap_or(0);
}
}
Ok(())
}
/// Saves the current values to `ispConfig.py` and store the
/// previous settings in `ispConfig.py.backup`.
///
pub fn save(&self) -> Result<()> {
// Find the config
let cfg = etc::EtcLqos::load()?;
let base_path = Path::new(&cfg.lqos_directory);
let final_path = base_path.clone().join("ispConfig.py");
let backup_path = base_path.join("ispConfig.py.backup");
std::fs::copy(&final_path, &backup_path)?;
// Load existing file
let original = read_to_string(&final_path)?;
// Temporary
//let final_path = base_path.join("ispConfig.py.test");
// Update config entries line by line
let mut config = String::new();
for line in original.split('\n') {
let mut line = line.to_string();
if line.starts_with("interfaceA") {
line = format!("interfaceA = '{}'", self.isp_interface);
}
if line.starts_with("interfaceB") {
line = format!("interfaceB = '{}'", self.internet_interface);
}
if line.starts_with("OnAStick") {
line = format!(
"OnAStick = {}",
if self.on_a_stick_mode {
"True"
} else {
"False"
}
);
}
if line.starts_with("StickVlanA") {
line = format!("StickVlanA = {}", self.stick_vlans.0);
}
if line.starts_with("StickVlanB") {
line = format!("StickVlanB = {}", self.stick_vlans.1);
}
if line.starts_with("sqm") {
line = format!("sqm = '{}'", self.sqm);
}
if line.starts_with("upstreamBandwidthCapacityDownloadMbps") {
line = format!(
"upstreamBandwidthCapacityDownloadMbps = {}",
self.total_download_mbps
);
}
if line.starts_with("upstreamBandwidthCapacityUploadMbps") {
line = format!(
"upstreamBandwidthCapacityUploadMbps = {}",
self.total_upload_mbps
);
}
if line.starts_with("monitorOnlyMode") {
line = format!(
"monitorOnlyMode = {}",
if self.monitor_mode { "True" } else { "False" }
);
}
if line.starts_with("generatedPNDownloadMbps") {
line = format!("generatedPNDownloadMbps = {}", self.generated_download_mbps);
}
if line.starts_with("generatedPNUploadMbps") {
line = format!("generatedPNUploadMbps = {}", self.generated_upload_mbps);
}
if line.starts_with("useBinPackingToBalanceCPU") {
line = format!(
"useBinPackingToBalanceCPU = {}",
if self.use_binpacking { "True" } else { "False" }
);
}
if line.starts_with("enableActualShellCommands") {
line = format!(
"enableActualShellCommands = {}",
if self.enable_shell_commands {
"True"
} else {
"False"
}
);
}
if line.starts_with("runShellCommandsAsSudo") {
line = format!(
"runShellCommandsAsSudo = {}",
if self.run_as_sudo { "True" } else { "False" }
);
}
if line.starts_with("queuesAvailableOverride") {
line = format!("queuesAvailableOverride = {}", self.override_queue_count);
}
config += &format!("{line}\n");
}
// Actually save to disk
if final_path.exists() {
remove_file(&final_path)?;
}
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&final_path)?;
file.write_all(&config.as_bytes())?;
Ok(())
}
}
fn split_at_equals(line: &str) -> String {
line.split('=')
.nth(1)
.unwrap_or("")
.trim()
.replace("\"", "")
.replace("'", "")
}

View File

@@ -0,0 +1,40 @@
use crate::etc;
use anyhow::{Error, Result};
use std::{
path::{Path, PathBuf},
process::Command,
};
const PYTHON_PATH: &str = "/usr/bin/python3";
fn path_to_libreqos() -> Result<PathBuf> {
let cfg = etc::EtcLqos::load()?;
let base_path = Path::new(&cfg.lqos_directory);
Ok(base_path.join("LibreQoS.py"))
}
fn working_directory() -> Result<PathBuf> {
let cfg = etc::EtcLqos::load()?;
let base_path = Path::new(&cfg.lqos_directory);
Ok(base_path.to_path_buf())
}
/// Shells out and reloads the `LibreQos.py` program, storing all
/// emitted text and returning it.
pub fn load_libreqos() -> Result<String> {
let path = path_to_libreqos()?;
if !path.exists() {
return Err(Error::msg("LibreQoS.py not found"));
}
if !Path::new(PYTHON_PATH).exists() {
return Err(Error::msg("Python not found"));
}
let result = Command::new(PYTHON_PATH)
.current_dir(working_directory()?)
.arg("LibreQoS.py")
.output()?;
let stdout = String::from_utf8(result.stdout)?;
let stderr = String::from_utf8(result.stderr)?;
Ok(stdout + &stderr)
}

View File

@@ -0,0 +1,216 @@
mod serializable;
mod shaped_device;
use crate::etc;
use anyhow::Result;
use csv::{QuoteStyle, WriterBuilder};
use serializable::SerializableShapedDevice;
pub use shaped_device::ShapedDevice;
use std::path::{Path, PathBuf};
/// Provides handling of the `ShapedDevices.csv` file that maps
/// circuits to traffic shaping.
pub struct ConfigShapedDevices {
/// List of all devices subject to traffic shaping.
pub devices: Vec<ShapedDevice>,
/// An LPM trie storing the IP mappings of all shaped devices,
/// allowing for quick IP-to-circuit mapping.
pub trie: ip_network_table::IpNetworkTable<usize>,
}
impl ConfigShapedDevices {
/// The path to the current `ShapedDevices.csv` file, determined
/// by acquiring the prefix from the `/etc/lqos` configuration
/// file.
pub fn path() -> Result<PathBuf> {
let cfg = etc::EtcLqos::load()?;
let base_path = Path::new(&cfg.lqos_directory);
Ok(base_path.join("ShapedDevices.csv"))
}
/// Loads `ShapedDevices.csv` and constructs a `ConfigShapedDevices`
/// object containing the resulting data.
pub fn load() -> Result<Self> {
let final_path = ConfigShapedDevices::path()?;
let mut reader = csv::Reader::from_path(final_path)?;
// Example: StringRecord(["1", "968 Circle St., Gurnee, IL 60031", "1", "Device 1", "", "", "192.168.101.2", "", "25", "5", "10000", "10000", ""])
let mut devices = Vec::new();
for result in reader.records() {
if let Ok(result) = result {
if let Ok(device) = ShapedDevice::from_csv(&result) {
devices.push(device);
}
}
}
let trie = ConfigShapedDevices::make_trie(&devices);
Ok(Self { devices, trie })
}
fn make_trie(devices: &[ShapedDevice]) -> ip_network_table::IpNetworkTable<usize> {
use ip_network::IpNetwork;
let mut table = ip_network_table::IpNetworkTable::new();
devices
.iter()
.enumerate()
.map(|(i, d)| (i, d.to_ipv6_list()))
.for_each(|(id, ips)| {
ips.iter().for_each(|(ip, cidr)| {
if let Ok(net) = IpNetwork::new(*ip, (*cidr) as u8) {
table.insert(net, id);
}
});
});
table
}
fn to_csv_string(&self) -> Result<String> {
let mut writer = WriterBuilder::new()
.quote_style(QuoteStyle::NonNumeric)
.from_writer(vec![]);
for d in self
.devices
.iter()
.map(|d| SerializableShapedDevice::from(d))
{
writer.serialize(d)?;
}
let data = String::from_utf8(writer.into_inner()?)?;
Ok(data)
}
/// Saves the current shaped devices list to `ShapedDevices.csv`
pub fn write_csv(&self, filename: &str) -> Result<()> {
let cfg = etc::EtcLqos::load()?;
let base_path = Path::new(&cfg.lqos_directory);
let path = base_path.join(filename);
let csv = self.to_csv_string()?;
std::fs::write(path, csv)?;
Ok(())
}
}
#[cfg(test)]
mod test {
use std::net::{Ipv4Addr, Ipv6Addr};
use super::*;
#[test]
fn test_simple_ipv4_parse() {
let (ip, cidr) = ShapedDevice::parse_cidr_v4("1.2.3.4").unwrap();
assert_eq!(cidr, 32);
assert_eq!("1.2.3.4".parse::<Ipv4Addr>().unwrap(), ip);
}
#[test]
fn test_cidr_ipv4_parse() {
let (ip, cidr) = ShapedDevice::parse_cidr_v4("1.2.3.4/24").unwrap();
assert_eq!(cidr, 24);
assert_eq!("1.2.3.4".parse::<Ipv4Addr>().unwrap(), ip);
}
#[test]
fn test_bad_ipv4_parse() {
let r = ShapedDevice::parse_cidr_v4("bad wolf");
assert!(r.is_err());
}
#[test]
fn test_nearly_ok_ipv4_parse() {
let r = ShapedDevice::parse_cidr_v4("192.168.1.256/32");
assert!(r.is_err());
}
#[test]
fn test_single_ipv4() {
let r = ShapedDevice::parse_ipv4("1.2.3.4");
assert_eq!(r.len(), 1);
assert_eq!(r[0].0, "1.2.3.4".parse::<Ipv4Addr>().unwrap());
assert_eq!(r[0].1, 32);
}
#[test]
fn test_two_ipv4() {
let r = ShapedDevice::parse_ipv4("1.2.3.4, 1.2.3.4/24");
assert_eq!(r.len(), 2);
assert_eq!(r[0].0, "1.2.3.4".parse::<Ipv4Addr>().unwrap());
assert_eq!(r[0].1, 32);
assert_eq!(r[1].0, "1.2.3.4".parse::<Ipv4Addr>().unwrap());
assert_eq!(r[1].1, 24);
}
#[test]
fn test_simple_ipv6_parse() {
let (ip, cidr) = ShapedDevice::parse_cidr_v6("fd77::1:5").unwrap();
assert_eq!(cidr, 128);
assert_eq!("fd77::1:5".parse::<Ipv6Addr>().unwrap(), ip);
}
#[test]
fn test_cidr_ipv6_parse() {
let (ip, cidr) = ShapedDevice::parse_cidr_v6("fd77::1:5/64").unwrap();
assert_eq!(cidr, 64);
assert_eq!("fd77::1:5".parse::<Ipv6Addr>().unwrap(), ip);
}
#[test]
fn test_bad_ipv6_parse() {
let r = ShapedDevice::parse_cidr_v6("bad wolf");
assert!(r.is_err());
}
#[test]
fn test_nearly_ok_ipv6_parse() {
let r = ShapedDevice::parse_cidr_v6("fd77::1::5");
assert!(r.is_err());
}
#[test]
fn test_single_ipv6() {
let r = ShapedDevice::parse_ipv6("fd77::1:5");
assert_eq!(r.len(), 1);
assert_eq!(r[0].0, "fd77::1:5".parse::<Ipv6Addr>().unwrap());
assert_eq!(r[0].1, 128);
}
#[test]
fn test_two_ipv6() {
let r = ShapedDevice::parse_ipv6("fd77::1:5, fd77::1:5/64");
assert_eq!(r.len(), 2);
assert_eq!(r[0].0, "fd77::1:5".parse::<Ipv6Addr>().unwrap());
assert_eq!(r[0].1, 128);
assert_eq!(r[1].0, "fd77::1:5".parse::<Ipv6Addr>().unwrap());
assert_eq!(r[1].1, 64);
}
#[test]
fn build_and_test_simple_trie() {
let devices = vec![
ShapedDevice {
circuit_id: "One".to_string(),
ipv4: ShapedDevice::parse_ipv4("192.168.1.0/24"),
..Default::default()
},
ShapedDevice {
circuit_id: "One".to_string(),
ipv4: ShapedDevice::parse_ipv4("1.2.3.4"),
..Default::default()
},
];
let trie = ConfigShapedDevices::make_trie(&devices);
assert_eq!(trie.len(), (0, 2));
assert!(trie
.longest_match(ShapedDevice::parse_cidr_v4("192.168.2.2").unwrap().0)
.is_none());
let addr: Ipv4Addr = "192.168.1.2".parse().unwrap();
let v6 = addr.to_ipv6_mapped();
assert!(trie.longest_match(v6).is_some());
let addr: Ipv4Addr = "1.2.3.4".parse().unwrap();
let v6 = addr.to_ipv6_mapped();
assert!(trie.longest_match(v6).is_some());
}
}

View File

@@ -0,0 +1,87 @@
use crate::ShapedDevice;
use serde::Serialize;
use std::net::{Ipv4Addr, Ipv6Addr};
// Example: StringRecord(["1", "968 Circle St., Gurnee, IL 60031", "1", "Device 1", "", "", "192.168.101.2", "", "25", "5", "10000", "10000", ""])
#[derive(Serialize)]
pub(crate) struct SerializableShapedDevice {
pub circuit_id: String,
pub circuit_name: String,
pub device_id: String,
pub device_name: String,
pub parent_node: String,
pub mac: String,
pub ipv4: String,
pub ipv6: String,
pub download_min_mbps: u32,
pub upload_min_mbps: u32,
pub download_max_mbps: u32,
pub upload_max_mbps: u32,
pub comment: String,
}
impl From<&ShapedDevice> for SerializableShapedDevice {
fn from(d: &ShapedDevice) -> Self {
Self {
circuit_id: d.circuit_id.clone(),
circuit_name: d.circuit_name.clone(),
device_id: d.device_id.clone(),
device_name: d.device_name.clone(),
parent_node: d.parent_node.clone(),
mac: d.mac.clone(),
ipv4: ipv4_list_to_string(&d.ipv4),
ipv6: ipv6_list_to_string(&d.ipv6),
download_min_mbps: d.download_min_mbps,
upload_min_mbps: d.upload_min_mbps,
download_max_mbps: d.download_max_mbps,
upload_max_mbps: d.upload_max_mbps,
comment: d.comment.clone(),
}
}
}
fn ipv4_to_string(ip: &(Ipv4Addr, u32)) -> String {
if ip.1 == 32 {
format!("{}", ip.0)
} else {
format! {"{}/{}", ip.0, ip.1}
}
}
fn ipv4_list_to_string(ips: &[(Ipv4Addr, u32)]) -> String {
if ips.len() == 0 {
return String::new();
}
if ips.len() == 1 {
return ipv4_to_string(&ips[0]);
}
let mut buffer = String::new();
for i in 0..ips.len() - 1 {
buffer += &format!("{}, ", ipv4_to_string(&ips[i]));
}
buffer += &ipv4_to_string(&ips[ips.len() - 1]);
String::new()
}
fn ipv6_to_string(ip: &(Ipv6Addr, u32)) -> String {
if ip.1 == 32 {
format!("{}", ip.0)
} else {
format! {"{}/{}", ip.0, ip.1}
}
}
fn ipv6_list_to_string(ips: &[(Ipv6Addr, u32)]) -> String {
if ips.len() == 0 {
return String::new();
}
if ips.len() == 1 {
return ipv6_to_string(&ips[0]);
}
let mut buffer = String::new();
for i in 0..ips.len() - 1 {
buffer += &format!("{}, ", ipv6_to_string(&ips[i]));
}
buffer += &ipv6_to_string(&ips[ips.len() - 1]);
String::new()
}

View File

@@ -0,0 +1,170 @@
use anyhow::{Error, Result};
use csv::StringRecord;
use serde::{Deserialize, Serialize};
use std::net::{Ipv4Addr, Ipv6Addr};
/// Represents a row in the `ShapedDevices.csv` file.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ShapedDevice {
// Circuit ID,Circuit Name,Device ID,Device Name,Parent Node,MAC,IPv4,IPv6,Download Min Mbps,Upload Min Mbps,Download Max Mbps,Upload Max Mbps,Comment
/// The ID of the circuit to which the device belongs. Circuits are 1:many,
/// multiple devices may be in a single circuit.
pub circuit_id: String,
/// The name of the circuit. Since we're in a flat file, circuit names
/// must match.
pub circuit_name: String,
/// The device identification, typically drawn from a management tool.
pub device_id: String,
/// The display name of the device.
pub device_name: String,
/// The parent node of the device, derived from `network.json`
pub parent_node: String,
/// The device's MAC address. This isn't actually used, it exists for
/// convenient mapping/seraching.
pub mac: String,
/// A list of all IPv4 addresses and CIDR subnets associated with the
/// device. For example, ("192.168.1.0", 24) is equivalent to
/// "192.168.1.0/24"
pub ipv4: Vec<(Ipv4Addr, u32)>,
/// A list of all IPv4 addresses and CIDR subnets associated with the
/// device.
pub ipv6: Vec<(Ipv6Addr, u32)>,
/// Minimum download: this is the bandwidth level the shaper will try
/// to ensure is always available.
pub download_min_mbps: u32,
/// Minimum upload: this is the bandwidth level the shaper will try to
/// ensure is always available.
pub upload_min_mbps: u32,
/// Maximum download speed, when possible.
pub download_max_mbps: u32,
/// Maximum upload speed when possible.
pub upload_max_mbps: u32,
/// Generic comments field, does nothing.
pub comment: String,
}
impl Default for ShapedDevice {
fn default() -> Self {
Self {
circuit_id: String::new(),
circuit_name: String::new(),
device_id: String::new(),
device_name: String::new(),
parent_node: String::new(),
mac: String::new(),
ipv4: Vec::new(),
ipv6: Vec::new(),
download_min_mbps: 0,
download_max_mbps: 0,
upload_min_mbps: 0,
upload_max_mbps: 0,
comment: String::new(),
}
}
}
impl ShapedDevice {
pub(crate) fn from_csv(record: &StringRecord) -> Result<Self> {
Ok(Self {
circuit_id: record[0].to_string(),
circuit_name: record[1].to_string(),
device_id: record[2].to_string(),
device_name: record[3].to_string(),
parent_node: record[4].to_string(),
mac: record[5].to_string(),
ipv4: ShapedDevice::parse_ipv4(&record[6]),
ipv6: ShapedDevice::parse_ipv6(&record[7]),
download_min_mbps: record[8].parse()?,
upload_min_mbps: record[9].parse()?,
download_max_mbps: record[10].parse()?,
upload_max_mbps: record[11].parse()?,
comment: record[12].to_string(),
})
}
pub(crate) fn parse_cidr_v4(address: &str) -> Result<(Ipv4Addr, u32)> {
if address.contains("/") {
let split: Vec<&str> = address.split("/").collect();
if split.len() != 2 {
return Err(Error::msg("Unable to parse IPv4"));
}
return Ok((split[0].parse()?, split[1].parse()?));
} else {
return Ok((address.parse()?, 32));
}
}
pub(crate) fn parse_ipv4(str: &str) -> Vec<(Ipv4Addr, u32)> {
let mut result = Vec::new();
if str.contains(",") {
for ip in str.split(",") {
let ip = ip.trim();
if let Ok((ipv4, subnet)) = ShapedDevice::parse_cidr_v4(ip) {
result.push((ipv4, subnet));
}
}
} else {
// No Commas
if let Ok((ipv4, subnet)) = ShapedDevice::parse_cidr_v4(str) {
result.push((ipv4, subnet));
}
}
result
}
pub(crate) fn parse_cidr_v6(address: &str) -> Result<(Ipv6Addr, u32)> {
if address.contains("/") {
let split: Vec<&str> = address.split("/").collect();
if split.len() != 2 {
return Err(Error::msg("Unable to parse IPv6"));
}
return Ok((split[0].parse()?, split[1].parse()?));
} else {
return Ok((address.parse()?, 128));
}
}
pub(crate) fn parse_ipv6(str: &str) -> Vec<(Ipv6Addr, u32)> {
let mut result = Vec::new();
if str.contains(",") {
for ip in str.split(",") {
let ip = ip.trim();
if let Ok((ipv6, subnet)) = ShapedDevice::parse_cidr_v6(ip) {
result.push((ipv6, subnet));
}
}
} else {
// No Commas
if let Ok((ipv6, subnet)) = ShapedDevice::parse_cidr_v6(str) {
result.push((ipv6, subnet));
}
}
result
}
pub(crate) fn to_ipv6_list(&self) -> Vec<(Ipv6Addr, u32)> {
let mut result = Vec::new();
for (ipv4, cidr) in &self.ipv4 {
result.push((ipv4.to_ipv6_mapped(), cidr + 96));
}
result.extend_from_slice(&self.ipv6);
result
}
}

View File

@@ -0,0 +1,20 @@
[package]
name = "lqos_node_manager"
version = "0.1.0"
edition = "2021"
[features]
default = ["equinix_tests"]
equinix_tests = []
[dependencies]
rocket = { version = "0.5.0-rc.2", features = [ "json", "msgpack", "uuid" ] }
rocket_async_compression = "0.2.0"
lazy_static = "1.4"
parking_lot = "0.12"
lqos_bus = { path = "../lqos_bus" }
lqos_config = { path = "../lqos_config" }
anyhow = "1"
sysinfo = "0"
notify = { version = "5.0.0", default-features = false, feature=["macos_kqueue"] } # Not using crossbeam because of Tokio
default-net = "0"

View File

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

View File

@@ -0,0 +1,130 @@
use anyhow::Error;
use lazy_static::*;
use lqos_config::{UserRole, WebUsers};
use parking_lot::Mutex;
use rocket::serde::{json::Json, Deserialize, Serialize};
use rocket::{
http::{Cookie, CookieJar, Status},
request::{FromRequest, Outcome},
Request,
};
lazy_static! {
static ref WEB_USERS: Mutex<Option<WebUsers>> = 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();
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::Failure((
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::Failure((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();
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();
if lock.is_none() {
if 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();
if let Some(users) = &*lock {
return Json(users.get_username(token.value()));
}
}
Json("Anonymous".to_string())
}

View File

@@ -0,0 +1,50 @@
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

@@ -0,0 +1,70 @@
use crate::{auth_guard::AuthGuard, cache_control::NoCache};
use default_net::get_interfaces;
use lqos_bus::{BUS_BIND_ADDRESS, BusSession, BusRequest, encode_request, decode_response};
use lqos_config::{EtcLqos, LibreQoSConfig, Tunables};
use rocket::{fs::NamedFile, serde::json::Json, tokio::{net::TcpStream, io::{AsyncReadExt, AsyncWriteExt}}};
// 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 mut result = Vec::new();
for eth in get_interfaces().iter() {
let mac = if let Some(mac) = &eth.mac_addr {
mac.to_string()
} else {
String::new()
};
result.push((eth.name.clone(), format!("{:?}", eth.if_type), mac));
}
NoCache::new(Json(result))
}
#[get("/api/python_config")]
pub async fn get_current_python_config(_auth: AuthGuard) -> NoCache<Json<LibreQoSConfig>> {
let config = lqos_config::LibreQoSConfig::load().unwrap();
println!("{:#?}", config);
NoCache::new(Json(config))
}
#[get("/api/lqosd_config")]
pub async fn get_current_lqosd_config(_auth: AuthGuard) -> NoCache<Json<EtcLqos>> {
let config = lqos_config::EtcLqos::load().unwrap();
println!("{:#?}", config);
NoCache::new(Json(config))
}
#[post("/api/python_config", data = "<config>")]
pub async fn update_python_config(_auth: AuthGuard, config: Json<LibreQoSConfig>) -> Json<String> {
config.save().unwrap();
Json("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
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
let test = BusSession {
auth_cookie: 1234,
requests: vec![BusRequest::UpdateLqosDTuning(period, (*tuning).clone())],
};
let msg = encode_request(&test).unwrap();
stream.write(&msg).await.unwrap();
// Receive reply
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await.unwrap();
let _reply = decode_response(&buf).unwrap();
// For now, ignore the reply.
Json("OK".to_string())
}

View File

@@ -0,0 +1,90 @@
#[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 queue_info;
#[launch]
fn rocket() -> _ {
//tracker::SHAPED_DEVICES.read().write_csv("ShapedDeviceWriteTest.csv").unwrap();
let server = rocket::build()
.attach(AdHoc::on_liftoff("Poll lqosd", |_| {
Box::pin(async move {
rocket::tokio::spawn(tracker::update_tracking());
})
}))
.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,
config_control::config_page,
// Our JS library
static_pages::lqos_js,
static_pages::lqos_css,
static_pages::klingon,
// API calls
tracker::current_throughput,
tracker::throughput_ring,
tracker::cpu_usage,
tracker::ram_usage,
tracker::top_10_downloaders,
tracker::worst_10_rtt,
tracker::rtt_histogram,
tracker::host_counts,
tracker::busy_quantile,
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,
queue_info::raw_queue_by_circuit,
queue_info::run_btest,
queue_info::circuit_info,
queue_info::current_circuit_throughput,
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_lqos_tuning,
auth_guard::create_first_user,
auth_guard::login,
auth_guard::admin_check,
static_pages::login_page,
auth_guard::username,
// Supporting files
static_pages::bootsrap_css,
static_pages::plotly_js,
static_pages::jquery_js,
static_pages::bootsrap_js,
static_pages::tinylogo,
static_pages::favicon,
static_pages::fontawesome_solid,
static_pages::fontawesome_webfont,
static_pages::fontawesome_woff,
],
);
// 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

@@ -0,0 +1,138 @@
use crate::auth_guard::AuthGuard;
use crate::cache_control::NoCache;
use crate::tracker::SHAPED_DEVICES;
use lqos_bus::{
decode_response, encode_request, BusRequest, BusResponse, BusSession, BUS_BIND_ADDRESS,
};
use rocket::response::content::RawJson;
use rocket::serde::Serialize;
use rocket::serde::json::Json;
use rocket::tokio::io::{AsyncReadExt, AsyncWriteExt};
use rocket::tokio::net::TcpStream;
use std::net::IpAddr;
#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
pub struct CircuitInfo {
pub name: String,
pub capacity: (u64, u64)
}
#[get("/api/circuit_info/<circuit_id>")]
pub async fn circuit_info(circuit_id: String, _auth: AuthGuard) -> NoCache<Json<CircuitInfo>> {
if let Some(device) = SHAPED_DEVICES
.read()
.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(Json(result))
} else {
let result = CircuitInfo {
name: "Nameless".to_string(),
capacity: (1_000_000, 1_000_000)
};
NoCache::new(Json(result))
}
}
#[get("/api/circuit_throughput/<circuit_id>")]
pub async fn current_circuit_throughput(
circuit_id: String,
_auth: AuthGuard,
) -> NoCache<Json<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
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
let test = BusSession {
auth_cookie: 1234,
requests: vec![BusRequest::GetHostCounter],
};
let msg = encode_request(&test).unwrap();
stream.write(&msg).await.unwrap();
// Receive reply
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await.unwrap();
let reply = decode_response(&buf).unwrap();
for msg in reply.responses.iter() {
match msg {
BusResponse::HostCounters(hosts) => {
let devices = SHAPED_DEVICES.read();
for (ip, down, up) in hosts.iter() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => *ip,
};
if let Some(c) = devices.trie.longest_match(lookup) {
if devices.devices[*c.1].circuit_id == circuit_id {
result.push((ip.to_string(), *down, *up));
}
}
}
}
_ => {}
}
}
NoCache::new(Json(result))
}
#[get("/api/raw_queue_by_circuit/<circuit_id>")]
pub async fn raw_queue_by_circuit(
circuit_id: String,
_auth: AuthGuard,
) -> NoCache<RawJson<String>> {
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
let test = BusSession {
auth_cookie: 1234,
requests: vec![BusRequest::GetRawQueueData(circuit_id)],
};
let msg = encode_request(&test).unwrap();
stream.write(&msg).await.unwrap();
// Receive reply
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await.unwrap();
let reply = decode_response(&buf).unwrap();
let result = match &reply.responses[0] {
BusResponse::RawQueueData(msg) => msg.clone(),
_ => "Unable to request queue".to_string(),
};
NoCache::new(RawJson(result))
}
#[cfg(feature = "equinix_tests")]
#[get("/api/run_btest")]
pub async fn run_btest() -> NoCache<RawJson<String>> {
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
let test = BusSession {
auth_cookie: 1234,
requests: vec![BusRequest::RequestLqosEquinixTest],
};
let msg = encode_request(&test).unwrap();
stream.write(&msg).await.unwrap();
// Receive reply
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await.unwrap();
let reply = decode_response(&buf).unwrap();
let result = match &reply.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

@@ -0,0 +1,92 @@
use crate::auth_guard::AuthGuard;
use crate::cache_control::NoCache;
use crate::tracker::SHAPED_DEVICES;
use lazy_static::*;
use lqos_bus::{
decode_response, encode_request, BusRequest, BusResponse, BusSession, BUS_BIND_ADDRESS,
};
use lqos_config::ShapedDevice;
use parking_lot::RwLock;
use rocket::serde::json::Json;
use rocket::tokio::io::{AsyncReadExt, AsyncWriteExt};
use rocket::tokio::net::TcpStream;
lazy_static! {
static ref RELOAD_REQUIRED: RwLock<bool> = RwLock::new(false);
}
#[get("/api/all_shaped_devices")]
pub fn all_shaped_devices(_auth: AuthGuard) -> NoCache<Json<Vec<ShapedDevice>>> {
NoCache::new(Json(SHAPED_DEVICES.read().devices.clone()))
}
#[get("/api/shaped_devices_count")]
pub fn shaped_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
NoCache::new(Json(SHAPED_DEVICES.read().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();
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();
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.read()))
}
#[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 mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await.unwrap();
let test = BusSession {
auth_cookie: 1234,
requests: vec![BusRequest::ReloadLibreQoS],
};
let msg = encode_request(&test).unwrap();
stream.write(&msg).await.unwrap();
// Receive reply
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await.unwrap();
let reply = decode_response(&buf).unwrap();
let result = match &reply.responses[0] {
BusResponse::ReloadLibreQoS(msg) => msg.clone(),
_ => "Unable to reload LibreQoS".to_string(),
};
*RELOAD_REQUIRED.write() = false;
NoCache::new(Json(result))
}

View File

@@ -0,0 +1,136 @@
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("/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())
}
#[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/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.ico")]
pub async fn favicon<'a>() -> LongCache<Option<NamedFile>> {
LongCache::new(NamedFile::open("static/favicon.ico").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

@@ -0,0 +1,12 @@
use lazy_static::*;
use parking_lot::RwLock;
lazy_static! {
/// Global storage of current CPU usage
pub static ref CPU_USAGE : RwLock<Vec<f32>> = RwLock::new(Vec::new());
}
lazy_static! {
/// Global storage of current RAM usage
pub static ref MEMORY_USAGE : RwLock<Vec<u64>> = RwLock::new(vec![0, 0]);
}

View File

@@ -0,0 +1,19 @@
use lazy_static::*;
use lqos_bus::IpStats;
use parking_lot::RwLock;
lazy_static! {
pub static ref TOP_10_DOWNLOADERS: RwLock<Vec<IpStats>> = RwLock::new(Vec::new());
}
lazy_static! {
pub static ref WORST_10_RTT: RwLock<Vec<IpStats>> = RwLock::new(Vec::new());
}
lazy_static! {
pub static ref RTT_HISTOGRAM: RwLock<Vec<u32>> = RwLock::new(Vec::new());
}
lazy_static! {
pub static ref HOST_COUNTS: RwLock<(u32, u32)> = RwLock::new((0, 0));
}

View File

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

View File

@@ -0,0 +1,18 @@
use lazy_static::*;
use lqos_bus::IpStats;
use lqos_config::ConfigShapedDevices;
use parking_lot::RwLock;
lazy_static! {
/// Global storage of the shaped devices csv data.
/// Updated by the file system watcher whenever
/// the underlying file changes.
pub static ref SHAPED_DEVICES : RwLock<ConfigShapedDevices> = RwLock::new(ConfigShapedDevices::load().unwrap());
}
lazy_static! {
/// Global storage of the shaped devices csv data.
/// Updated by the file system watcher whenever
/// the underlying file changes.
pub static ref UNKNOWN_DEVICES : RwLock<Vec<IpStats>> = RwLock::new(Vec::new());
}

View File

@@ -0,0 +1,73 @@
use lazy_static::*;
use parking_lot::RwLock;
use rocket::serde::Serialize;
lazy_static! {
/// Global storage of the current throughput counter.
pub static ref CURRENT_THROUGHPUT : RwLock<ThroughputPerSecond> = RwLock::new(ThroughputPerSecond::default());
}
lazy_static! {
/// Global storage of the last N seconds throughput buffer.
pub static ref THROUGHPUT_BUFFER : RwLock<ThroughputRingbuffer> = RwLock::new(ThroughputRingbuffer::new());
}
/// Stores total system throughput per second.
#[derive(Debug, Clone, Copy, Serialize)]
#[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),
}
impl Default for ThroughputPerSecond {
fn default() -> Self {
Self {
bits_per_second: (0, 0),
packets_per_second: (0, 0),
shaped_bits_per_second: (0, 0),
}
}
}
/// How many entries (at one per second) should we keep in the
/// throughput ringbuffer?
const RINGBUFFER_SAMPLES: usize = 300;
/// Stores Throughput samples in a ringbuffer, continually
/// updating. There are always RINGBUFFER_SAMPLES available,
/// allowing for non-allocating/non-growing storage of
/// throughput for the dashboard summaries.
pub struct ThroughputRingbuffer {
readings: Vec<ThroughputPerSecond>,
next: usize,
}
impl ThroughputRingbuffer {
fn new() -> Self {
Self {
readings: vec![ThroughputPerSecond::default(); RINGBUFFER_SAMPLES],
next: 0,
}
}
pub fn store(&mut self, reading: ThroughputPerSecond) {
self.readings[self.next] = reading;
self.next += 1;
self.next %= RINGBUFFER_SAMPLES;
}
pub fn get_result(&self) -> Vec<ThroughputPerSecond> {
let mut result = Vec::new();
for i in self.next..RINGBUFFER_SAMPLES {
result.push(self.readings[i]);
}
for i in 0..self.next {
result.push(self.readings[i]);
}
result
}
}

View File

@@ -0,0 +1,151 @@
//! 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_bus::{
decode_response, encode_request, BusRequest, BusResponse, BusSession, IpStats, BUS_BIND_ADDRESS,
};
use lqos_config::ConfigShapedDevices;
use rocket::tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
task::spawn_blocking,
};
use std::{net::IpAddr, 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::CpuExt;
use sysinfo::System;
use sysinfo::SystemExt;
let mut sys = System::new_all();
spawn_blocking(|| {
let _ = watch_for_shaped_devices_changing();
});
loop {
//println!("Updating tracking data");
sys.refresh_cpu();
sys.refresh_memory();
let cpu_usage = sys
.cpus()
.iter()
.map(|cpu| cpu.cpu_usage())
.collect::<Vec<f32>>();
*CPU_USAGE.write() = cpu_usage;
{
let mut mem_use = MEMORY_USAGE.write();
mem_use[0] = sys.used_memory();
mem_use[1] = sys.total_memory();
}
let _ = get_data_from_server().await; // Ignoring errors to keep running
rocket::tokio::time::sleep(Duration::from_secs(1)).await;
}
}
/// Fires up a Linux file system watcher than notifies
/// when `ShapedDevices.csv` changes, and triggers a reload.
fn watch_for_shaped_devices_changing() -> Result<()> {
use notify::{Config, RecursiveMode, Watcher};
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = notify::RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(&ConfigShapedDevices::path()?, RecursiveMode::NonRecursive)?;
loop {
let _ = rx.recv();
if let Ok(new_file) = ConfigShapedDevices::load() {
println!("ShapedDevices.csv changed");
*SHAPED_DEVICES.write() = new_file;
}
}
}
/// Requests data from `lqosd` and stores it in local
/// caches.
async fn get_data_from_server() -> Result<()> {
// Send request to lqosd
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await?;
let test = BusSession {
auth_cookie: 1234,
requests: vec![
BusRequest::GetCurrentThroughput,
BusRequest::GetTopNDownloaders(10),
BusRequest::GetWorstRtt(10),
BusRequest::RttHistogram,
BusRequest::AllUnknownIps,
],
};
let msg = encode_request(&test)?;
stream.write(&msg).await?;
// Receive reply
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await.unwrap();
let reply = decode_response(&buf)?;
// Process the reply
for r in reply.responses.iter() {
match r {
BusResponse::CurrentThroughput {
bits_per_second,
packets_per_second,
shaped_bits_per_second,
} => {
{
let mut lock = CURRENT_THROUGHPUT.write();
lock.bits_per_second = *bits_per_second;
lock.packets_per_second = *packets_per_second;
} // Lock scope
{
let mut lock = THROUGHPUT_BUFFER.write();
lock.store(ThroughputPerSecond {
packets_per_second: *packets_per_second,
bits_per_second: *bits_per_second,
shaped_bits_per_second: *shaped_bits_per_second,
});
}
}
BusResponse::TopDownloaders(stats) => {
*TOP_10_DOWNLOADERS.write() = stats.clone();
}
BusResponse::WorstRtt(stats) => {
*WORST_10_RTT.write() = stats.clone();
}
BusResponse::RttHistogram(stats) => {
*RTT_HISTOGRAM.write() = stats.clone();
}
BusResponse::AllUnknownIps(unknowns) => {
*HOST_COUNTS.write() = (unknowns.len() as u32, 0);
let cfg = SHAPED_DEVICES.read();
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();
*HOST_COUNTS.write() = (really_unknown.len() as u32, 0);
*UNKNOWN_DEVICES.write() = really_unknown;
}
// Default
_ => {}
}
}
Ok(())
}

View File

@@ -0,0 +1,135 @@
mod cache;
mod cache_manager;
use self::cache::{
CPU_USAGE, CURRENT_THROUGHPUT, HOST_COUNTS, MEMORY_USAGE, RTT_HISTOGRAM, THROUGHPUT_BUFFER,
TOP_10_DOWNLOADERS, WORST_10_RTT,
};
use crate::{auth_guard::AuthGuard, tracker::cache::ThroughputPerSecond};
pub use cache::{SHAPED_DEVICES, UNKNOWN_DEVICES};
pub use cache_manager::update_tracking;
use lazy_static::lazy_static;
use lqos_bus::{IpStats, TcHandle};
use lqos_config::LibreQoSConfig;
use parking_lot::Mutex;
use rocket::serde::{json::Json, Deserialize, Serialize};
use std::net::IpAddr;
#[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),
}
impl From<&IpStats> for IpStatsWithPlan {
fn from(i: &IpStats) -> Self {
let mut result = Self {
ip_address: i.ip_address.clone(),
bits_per_second: i.bits_per_second,
packets_per_second: i.packets_per_second,
median_tcp_rtt: i.median_tcp_rtt,
tc_handle: i.tc_handle,
circuit_id: String::new(),
plan: (0, 0),
};
if let Ok(ip) = result.ip_address.parse::<IpAddr>() {
let lookup = match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
};
let cfg = SHAPED_DEVICES.read();
if let Some((_, id)) = cfg.trie.longest_match(lookup) {
result.ip_address =
format!("{} ({})", cfg.devices[*id].circuit_name, result.ip_address);
result.plan.0 = cfg.devices[*id].download_max_mbps;
result.plan.1 = cfg.devices[*id].upload_max_mbps;
result.circuit_id = cfg.devices[*id].circuit_id.clone();
}
}
result
}
}
#[get("/api/current_throughput")]
pub fn current_throughput(_auth: AuthGuard) -> Json<ThroughputPerSecond> {
let result = CURRENT_THROUGHPUT.read().clone();
Json(result)
}
#[get("/api/throughput_ring")]
pub fn throughput_ring(_auth: AuthGuard) -> Json<Vec<ThroughputPerSecond>> {
let result = THROUGHPUT_BUFFER.read().get_result();
Json(result)
}
#[get("/api/cpu")]
pub fn cpu_usage(_auth: AuthGuard) -> Json<Vec<f32>> {
let cpu_usage = CPU_USAGE.read().clone();
Json(cpu_usage)
}
#[get("/api/ram")]
pub fn ram_usage(_auth: AuthGuard) -> Json<Vec<u64>> {
let ram_usage = MEMORY_USAGE.read().clone();
Json(ram_usage)
}
#[get("/api/top_10_downloaders")]
pub fn top_10_downloaders(_auth: AuthGuard) -> Json<Vec<IpStatsWithPlan>> {
let tt: Vec<IpStatsWithPlan> = TOP_10_DOWNLOADERS
.read()
.iter()
.map(|tt| tt.into())
.collect();
Json(tt)
}
#[get("/api/worst_10_rtt")]
pub fn worst_10_rtt(_auth: AuthGuard) -> Json<Vec<IpStatsWithPlan>> {
let tt: Vec<IpStatsWithPlan> = WORST_10_RTT.read().iter().map(|tt| tt.into()).collect();
Json(tt)
}
#[get("/api/rtt_histogram")]
pub fn rtt_histogram(_auth: AuthGuard) -> Json<Vec<u32>> {
Json(RTT_HISTOGRAM.read().clone())
}
#[get("/api/host_counts")]
pub fn host_counts(_auth: AuthGuard) -> Json<(u32, u32)> {
let shaped_reader = SHAPED_DEVICES.read();
let n_devices = shaped_reader.devices.len();
let host_counts = HOST_COUNTS.read();
let unknown = host_counts.0 - host_counts.1;
Json((n_devices as u32, unknown))
}
lazy_static! {
static ref CONFIG: Mutex<LibreQoSConfig> = Mutex::new(lqos_config::LibreQoSConfig::load().unwrap());
}
#[get("/api/busy_quantile")]
pub fn busy_quantile(_auth: AuthGuard) -> Json<Vec<(u32, u32)>> {
let (down_capacity, up_capacity) = {
let lock = CONFIG.lock();
(lock.total_download_mbps as f64 * 1_000_000.0, lock.total_upload_mbps as f64 * 1_000_000.0)
};
let throughput = THROUGHPUT_BUFFER.read().get_result();
let mut result = vec![(0,0); 10];
throughput.iter().for_each(|tp| {
let (down, up) = tp.bits_per_second;
let (down, up) = (down * 8, up * 8);
//println!("{down_capacity}, {up_capacity}, {down}, {up}");
let (down, up) = (down as f64 / down_capacity, up as f64 / up_capacity);
let (down, up) = ((down * 10.0) as usize, (up * 10.0) as usize);
result[usize::min(9, down)].0 += 1;
result[usize::min(0, up)].1 += 1;
});
Json(result)
}

View File

@@ -0,0 +1,24 @@
use crate::{auth_guard::AuthGuard, cache_control::NoCache, tracker::UNKNOWN_DEVICES};
use lqos_bus::IpStats;
use rocket::serde::json::Json;
#[get("/api/all_unknown_devices")]
pub fn all_unknown_devices(_auth: AuthGuard) -> NoCache<Json<Vec<IpStats>>> {
NoCache::new(Json(UNKNOWN_DEVICES.read().clone()))
}
#[get("/api/unknown_devices_count")]
pub fn unknown_devices_count(_auth: AuthGuard) -> NoCache<Json<usize>> {
NoCache::new(Json(UNKNOWN_DEVICES.read().len()))
}
#[get("/api/unknown_devices_range/<start>/<end>")]
pub fn unknown_devices_range(
start: usize,
end: usize,
_auth: AuthGuard,
) -> NoCache<Json<Vec<IpStats>>> {
let reader = UNKNOWN_DEVICES.read();
let result: Vec<IpStats> = reader.iter().skip(start).take(end).cloned().collect();
NoCache::new(Json(result))
}

View File

@@ -0,0 +1,446 @@
<!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">
<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>
</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="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</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 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 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>
</ul>
</div>
<div class="col-sm-2">
<div id="raw"></div>
</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-bar-chart"></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-bar-chart"></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</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-bar-chart"></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-bar-chart"></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="0">
<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-bar-chart"></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-bar-chart"></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-bar-chart"></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-bar-chart"></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>
</div>
<footer>Copyright (c) 2022, LibreQoE LLC</footer>
<script>
let throughput = new Object();
let throughput_head = 0;
let circuit_info = null;
function setX(x, counter) {
for (let i=0; i<x.length; i++) {
x[i].push(counter);
}
}
function setY(y, i, data, tin) {
if (data[0] == "None" || data[1] == "None") {
for (let j=0; i<y.length; y++) {
y[j].push(0);
}
} else {
y[0].push(data[0].Cake.tins[tin].sent_bytes * 8); // Download
y[1].push(0.0 - (data[1].Cake.tins[tin].sent_bytes * 8)); // Upload
y[2].push(data[0].Cake.tins[tin].drops); // Down Drops
y[3].push(data[0].Cake.tins[tin].marks); // Down Marks
y[4].push(0.0 - data[1].Cake.tins[tin].drops); // Up Drops
y[5].push(0.0 - data[1].Cake.tins[tin].marks); // Up Marks
// Backlog
y[6].push(data[0].Cake.tins[tin].backlog_bytes * 8);
y[7].push(0.0 - data[1].Cake.tins[tin].backlog_bytes * 8);
// Delays
y[8].push(data[0].Cake.tins[tin].avg_delay_us);
y[9].push(0.0 - data[1].Cake.tins[tin].avg_delay_us);
}
}
function pollQueue() {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params.id != null) {
// Name the circuit
$.get("/api/circuit_info/" + encodeURI(params.id), (data) => {
circuit_info = data;
$("#circuitName").text(redactText(circuit_info.name));
});
// Fill the raw button
$("#raw").html("<a class='btn btn-sm btn-info' href='/api/raw_queue_by_circuit/" + encodeURI(params.id) + "'><i class='fa fa-search'></i> Raw Data</a>");
// Graphs
$.get("/api/raw_queue_by_circuit/" + encodeURI(params.id), (data) => {
// Fill Base Information
let total_memory = data.current_download.Cake.memory_used + data.current_upload.Cake.memory_used;
$("#memory").text(scaleNumber(total_memory));
// Fill Tin Graphs
let backlogX1 = [];
let backlogY1 = [];
let backlogX2 = [];
let backlogY2 = [];
let delaysX1 = [];
let delaysX2 = [];
let delaysY1 = [];
let delaysY2 = [];
let qlenX1 = [];
let qlenY1 = [];
let qlenX2 = [];
let qlenY2 = [];
for (let tin=0; tin<4; tin++) {
let entries = {
x: [[], [], [], [], [], [], [], [], [], []],
y: [[], [], [], [], [], [], [], [], [], []],
}
let counter = 0;
for (let i=data.history_head; i<600; i++) {
setX(entries.x, counter);
setY(entries.y, i, data.history[i], tin);
if (tin == 0) {
qlenX1.push(counter);
qlenX2.push(counter);
qlenY1.push(data.history[i][0].Cake.qlen);
qlenY2.push(0.0 - data.history[i][1].Cake.qlen);
}
counter++;
}
for (let i=0; i<data.history_head; i++) {
setX(entries.x, counter);
setY(entries.y, i, data.history[i], tin);
if (tin == 0) {
qlenX1.push(counter);
qlenX2.push(counter);
qlenY1.push(data.history[i][0].Cake.qlen);
qlenY2.push(0.0 - data.history[i][1].Cake.qlen);
}
counter++;
}
let graph = document.getElementById("tinTp_" + tin);
let graph_data = [
{x: entries.x[0], y:entries.y[0], name: 'Download', type: 'scatter'},
{x: entries.x[1], y:entries.y[1], name: 'Upload', type: 'scatter'},
];
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true} });
graph = document.getElementById("tinMd_" + tin);
graph_data = [
{x: entries.x[2], y:entries.y[2], name: 'Down Drops', type: 'scatter'},
{x: entries.x[3], y:entries.y[3], name: 'Down Marks', type: 'scatter'},
{x: entries.x[4], y:entries.y[4], name: 'Up Drops', type: 'scatter'},
{x: entries.x[5], y:entries.y[5], name: 'Up Marks', type: 'scatter'},
];
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true} });
backlogX1.push(entries.x[6]);
backlogX2.push(entries.x[7]);
backlogY1.push(entries.y[6]);
backlogY2.push(entries.y[7]);
delaysX1.push(entries.x[8]);
delaysX2.push(entries.x[9]);
delaysY1.push(entries.y[8]);
delaysY2.push(entries.y[9]);
}
let graph = document.getElementById("backlogGraph");
let graph_data = [
{x: backlogX1[0], y:backlogY1[0], type: 'scatter', name: 'Tin 0 Down'},
{x: backlogX2[0], y:backlogY2[0], type: 'scatter', name: 'Tin 0 Up'},
{x: backlogX1[1], y:backlogY1[1], type: 'scatter', name: 'Tin 1 Down'},
{x: backlogX2[1], y:backlogY2[1], type: 'scatter', name: 'Tin 1 Up'},
{x: backlogX1[2], y:backlogY1[2], type: 'scatter', name: 'Tin 2 Down'},
{x: backlogX2[2], y:backlogY2[2], type: 'scatter', name: 'Tin 2 Up'},
{x: backlogX1[3], y:backlogY1[3], type: 'scatter', name: 'Tin 3 Down'},
{x: backlogX2[3], y:backlogY2[3], type: 'scatter', name: 'Tin 3 Up'},
];
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true} });
graph = document.getElementById("delayGraph");
graph_data = [
{x: delaysX1[0], y:delaysY1[0], type: 'scatter', name: 'Tin 0 Down'},
{x: delaysX2[0], y:delaysY2[0], type: 'scatter', name: 'Tin 0 Up'},
{x: delaysX1[1], y:delaysY1[1], type: 'scatter', name: 'Tin 1 Down'},
{x: delaysX2[1], y:delaysY2[1], type: 'scatter', name: 'Tin 1 Up'},
{x: delaysX1[2], y:delaysY1[2], type: 'scatter', name: 'Tin 2 Down'},
{x: delaysX2[2], y:delaysY2[2], type: 'scatter', name: 'Tin 2 Up'},
{x: delaysX1[3], y:delaysY1[3], type: 'scatter', name: 'Tin 3 Down'},
{x: delaysX2[3], y:delaysY2[3], type: 'scatter', name: 'Tin 3 Up'},
];
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true} });
graph = document.getElementById("qlenGraph");
graph_data = [
{x: qlenX1, y:qlenY1, type: 'scatter', name: 'Down'},
{x: qlenX2, y:qlenY2, type: 'scatter', name: 'Up'},
];
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true} });
});
}
setTimeout(pollQueue, 1000);
}
function getThroughput() {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params.id != null) {
$.get("/api/circuit_throughput/" + encodeURI(params.id), (data) => {
let 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],
];
for (let i=0; i<data.length; i++) {
let ip = data[i][0];
let down = data[i][1];
let up = data[i][2];
if (throughput[ip] == null) {
throughput[ip] = {
down: [],
up: [],
};
for (let j=0; j<300; j++) {
throughput[ip].down.push(0);
throughput[ip].up.push(0);
}
}
throughput[ip].down[throughput_head] = down * 8;
throughput[ip].up[throughput_head] = up * 8;
}
// Build the graph
let graph = document.getElementById("throughputGraph");
let graph_data = [];
for (const ip in throughput) {
//console.log(ip);
let xUp = [];
let xDown = [];
let yUp = [];
let yDown = [];
let counter = 0;
for (let i=throughput_head; i<300; i++) {
xUp.push(counter);
xDown.push(counter);
yUp.push(0.0 - throughput[ip].up[i]);
yDown.push(throughput[ip].down[i]);
counter++;
}
for (let i=0; i<throughput_head; i++) {
xUp.push(counter);
xDown.push(counter);
yUp.push(0.0 - throughput[ip].up[i]);
yDown.push(throughput[ip].down[i]);
counter++;
}
// Build the quantiles graph
if (circuit_info != null) {
for (let i=0; i<yDown.length; i++) {
let down = yDown[i];
let up = 0.0 - yUp[i];
let down_slot = Math.floor((down / circuit_info.capacity[0]) * 10.0);
let up_slot = Math.floor((up / circuit_info.capacity[1]) * 10.0);
if (down_slot > 0) quantiles[0][down_slot] += 1;
if (up_slot > 0) quantiles[1][up_slot] += 1;
}
}
graph_data.push({x: xDown, y: yDown, name: ip + " Down", type: 'scatter'});
graph_data.push({x: xUp, y: yUp, name: ip + " Up", type: 'scatter'});
}
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true} });
throughput_head += 1;
if (throughput_head >= 300) {
throughput_head = 0;
}
graph = document.getElementById("capacityQuantile");
graph_data = [
{x: quantiles[2], y: quantiles[0], name: 'Download', type: 'bar'},
{x: quantiles[2], y: quantiles[1], name: 'Upload', type: 'bar'},
];
Plotly.newPlot(graph, graph_data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: '# Samples' }, xaxis: {automargin: true, title: '% Utilization'} });
});
}
setTimeout(getThroughput, 1000);
}
function start() {
colorReloadButton();
updateHostCounts();
pollQueue();
getThroughput();
}
$(document).ready(start);
</script>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,642 @@
<!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">
<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>
</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="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</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 ms-auto">
<a class="nav-link active" 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-2"></div>
<div class="col-sm-8">
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title"><i class="fa fa-users"></i> Configuration</h5>
<div class="col-sm-8 mx-auto" class="pad4 mbot4" id="controls">
<a href="#" class="btn btn-primary" id="btnSaveIspConfig"><i class="fa fa-save"></i> Save ispConfig.py</a>
</div>
<div class="d-flex align-items-start">
<div class="nav flex-column nav-pills me-3" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<button class="nav-link active" id="v-pills-display-tab" data-bs-toggle="pill" data-bs-target="#v-pills-display" type="button" role="tab" aria-controls="v-pills-home" aria-selected="true"><i class="fa fa-tv"></i> Display</button>
<button class="nav-link" id="v-pills-home-tab" data-bs-toggle="pill" data-bs-target="#v-pills-home" type="button" role="tab" aria-controls="v-pills-home" aria-selected="true"><i class="fa fa-wifi"></i> Network</button>
<button class="nav-link" id="v-pills-shaper-tab" data-bs-toggle="pill" data-bs-target="#v-pills-shaper" type="button" role="tab" aria-controls="v-pills-profile" aria-selected="false"><i class="fa fa-balance-scale"></i> Shaper</button>
<button class="nav-link" id="v-pills-server-tab" data-bs-toggle="pill" data-bs-target="#v-pills-server" type="button" role="tab" aria-controls="v-pills-server" aria-selected="false"><i class="fa fa-server"></i> Server</button>
<button class="nav-link" id="v-pills-tuning-tab" data-bs-toggle="pill" data-bs-target="#v-pills-tuning" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-warning"></i> Tuning</button>
<button class="nav-link" id="v-pills-spylnx-tab" data-bs-toggle="pill" data-bs-target="#v-pills-spylnx" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-eye"></i> Spylnx</button>
<button class="nav-link" id="v-pills-uisp-tab" data-bs-toggle="pill" data-bs-target="#v-pills-uisp" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-eye"></i> UISP</button>
<button class="nav-link" id="v-pills-users-tab" data-bs-toggle="pill" data-bs-target="#v-pills-users" type="button" role="tab" aria-controls="v-pills-settings" aria-selected="false"><i class="fa fa-users"></i> LibreQoS Users</button>
</div>
<div class="tab-content" id="v-pills-tabContent">
<div class="tab-pane fade show active" id="v-pills-display" role="tabpanel" aria-labelledby="v-pills-display-tab">
<h2><i class="fa fa-wifi"></i> Display Settings</h2>
<table class="table">
<tr>
<td>
<p class="alert alert-info" role="alert">
<i class="fa fa-info"></i> The settings on this tab are per-computer and are stored locally in your browser's "local storage".
</p>
</td>
</tr>
<tr>
<td>
<input class="form-check-input" type="checkbox" id="redact">
<label class="form-check-label" for="redact">
Redact Customer Information (screenshot mode)
</label>
</td>
</tr>
<tr>
<td>
<select id="colorMode">
<option id="0">Regular Colors</option>
<option id="1">Metaverse Colors</option>
</select>
<label class="form-select-label" for="colorMode">
RTT Color Mode
</label>
</td>
</tr>
<tr>
<td><a class="btn btn-primary" id="applyDisplay">Apply Changes</a></td>
</tr>
</table>
</div>
<div class="tab-pane fade" id="v-pills-home" role="tabpanel" aria-labelledby="v-pills-home-tab">
<h2><i class="fa fa-wifi"></i> Network Settings</h2>
<table class="table">
<tr>
<td colspan="2">Setup the basic network interface configuration.</td>
</tr>
<tr>
<td colspan="2" class="alert alert-info" role="alert">
<i class="fa fa-info"></i> For normal operation, you need one NIC port facing the Internet, and a second facing your core router.
If you are operating in "on a stick" mode (with a single NIC, and VLANs for inbound and outbound),
select the same NIC for both directions.
</td>
</tr>
<tr>
<td>Interface facing your core router</td>
<td><select id="nicCore"></option></td>
</tr>
<tr>
<td>Interface facing the Internet</td>
<td><select id="nicInternet"></option></td>
</tr>
<tr>
<td colspan="2"><h3>Single-Interface ("On A Stick") Configuration</td>
</tr>
<tr>
<td colspan="2" class="alert alert-info" role="alert">
<i class="fa fa-info"></i> "On a stick" mode allows you to operate with a single NIC, with one VLAN
containing inbound traffic and the other outbound. Please refer to the
documentation.
</td>
</tr>
<tr colspan="2">
<td>
<input class="form-check-input" type="checkbox" value="" id="onAStick">
<label class="form-check-label" for="onAStick">
Enable Single-Interface ("on a stick") mode?
</label>
</td>
</tr>
<tr>
<td>VLAN facing your core router</td>
<td>
<input class="form-input" type="number" min="0" max="4094" id="StickVLANCore" />
</td>
</tr>
<tr>
<td>VLAN facing the Internet</td>
<td>
<input class="form-input" type="number" min="0" max="4094" id="StickVLANInternet" />
</td>
</tr>
<tr>
<td colspan="2">
<h3>Bifrost XDP-Accelerated Bridge</h3>
<p class="alert alert-danger" role="alert">
You must configure XDP bridging by editing the `/etc/lqos` file on the server.
</p>
</td>
</tr>
<tr>
<td colspan="2" class="alert alert-warning" role="alert">
<i class="fa fa-warning"></i> Bifrost is an experimental feature at this time. Bifrost XDP allows you to bypass the entire
Linux bridge system, and use XDP to bridge directly between interfaces or VLANs. This can result
in significant performance improvements on NICs that support XDP in "driver" mode.
</td>
</tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="useKernelBridge" disabled="true">
<label class="form-check-label" for="useKernelBridge">
Enable Bifrost Acceleration
</label>
</td>
</tr>
<tr>
<td id="bifrostInterfaces" colspan="2">Interface Mapping</td>
</tr>
<tr>
<td id="bifrostVlans" colspan="2">VLAN Mapping</td>
</tr>
</table>
</div>
<div class="tab-pane fade" id="v-pills-shaper" role="tabpanel" aria-labelledby="v-pills-shaper-tab">
<h2><i class="fa fa-balance-scale"></i>Shaper Settings</h2>
<p>Tune the LibreQoS traffic shaper to your needs.</p>
<table class="table">
<tr>
<td colspan="2">
<h3>Traffic Shaping Control</h3>
</td>
</tr>
<tr class="alert-info alert" role="alert">
<td colspan="2"><i class="fa fa-info"></i> FQ_CODEL offers good latency management and low CPU overhead. CAKE requires more CPU, but offers excellent latency management.</td>
</tr>
<tr>
<td>SQM Mode</td>
<td>
<select id="sqmMode">
<option value="fq_codel">FQ_Codel</option>
<option value="cake diffserv4">Cake + Diffserv4</option>
<option value="cake diffserv4 ackfilter">Cake + Diffserv4 + ACK Filter</option>
</select>
</td>
</tr>
<tr class="alert-info">
<td colspan="2">Monitor mode disables all traffic shaping, allowing you to watch your network undisturbed.</td>
</tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="monitorMode">
<label class="form-check-label" for="monitorMode">
Enable Monitor Mode
</label>
</td>
</tr>
<tr>
<td colspan="2">
<h3>Bandwidth</h3>
</td>
</tr>
<tr>
<td>Total Download Bandwidth (Mbps)</td>
<td><input type="number" min="1" max="1000000000" step="100" id="maxDownload"></td>
</tr>
<tr>
<td>Total Upload Bandwidth (Mbps)</td>
<td><input type="number" min="1" max="1000000000" step="100" id="maxUpload"></td>
</tr>
<tr class="alert-info">
<td colspan="2">
<i class="fa fa-info"></i> Devices without a parent will be placed underneath evenly-balanced generated nodes. This defines the
available bandwidth for those nodes. If in doubt, set to equal your total bandwidth.
</td>
</tr>
<tr>
<td>Generated Node Download Bandwidth (Mbps)</td>
<td><input type="number" min="1" max="1000000000" step="100" id="generatedDownload"></td>
</tr>
<tr>
<td>Generated Node Upload Bandwidth (Mbps)</td>
<td><input type="number" min="1" max="1000000000" step="100" id="generatedUpload"></td>
</tr>
<tr class="alert-info">
<td colspan="2">
Bin packing is only useful for devices without parent nodes in the shaper tree. Enable this option
to automatically assign devices to nodes based on the device's plans, evenly balancing load across
CPUs.
</td>
</tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="binpacking">
<label class="form-check-label" for="binpacking">
Use Binpacking
</label>
</td>
</table>
</div>
<div class="tab-pane fade" id="v-pills-server" role="tabpanel" aria-labelledby="v-pills-server-tab">
<h2><i class="fa fa-server"></i> Server Settings</h2>
<table class="table">
<tr>
<td colspan="2" class="alert-danger">
<i class="fa fa-warning"></i> Disabling actual shell commands stops LibreQoS from actually doing anything. Simulated
output is logged to the console and text files, allowing for debugging.
</td>
</tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="actualShellCommands">
<label class="form-check-label" for="actualShellCommands">
Enable Actual Shell Commands
</label>
</td>
</tr>
<tr>
<td colspan="2" class="alert-info">
<i class="fa fa-info"></i> Running shell commands with "sudo" isn't necessary on a default configuration.
</td>
</tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="useSudo">
<label class="form-check-label" for="useSudo">
Run Shell Commands as Sudo
</label>
</td>
</tr>
<tr>
<td colspan="2" class="alert-danger">
<i class="fa fa-warning"></i> Overriding the number of queues is only necessary if your NIC is giving
very strange results. Use with extreme caution. Leave this at 0 unless you really know what you are doing.
</td>
</tr>
<tr>
<td>Override count of available queues?</td>
<td><input type="number" min="2" max="256" step="2" id="overrideQueues" /></td>
</tr>
</table>
</div>
<div class="tab-pane fade" id="v-pills-tuning" role="tabpanel" aria-labelledby="v-pills-tuning-tab">
<h2><i class="fa fa-warning"></i> Tuning Settings</h2>
<table class="table">
<tr>
<td colspan="2" class="alert alert-danger" role="alert">
<i class="fa fa-warning"></i> <strong>DANGER</strong>
<p>These settings can drastically affect performance of your server, including rendering it non-functional.</p>
</td>
</tr>
<tr>
<td colspan="2" class="alert-info" role="alert">
How frequently should the TC queues be polled? 30-50 is good for detailed analysis,
1000 is good for normal running. If you select a value slower than the time currently taken
to access queue information, queue analysis will no longer display data on a consistent
time-step. Values less than 20ms are not recommended.
</td>
</tr>
<tr>
<td>
Queue Check Frequency (ms)
</td>
<td>
<input type="number" min="10" max="1000" id="queuecheckms" />
</td>
</tr>
<tr><td colspan="2">IRQ Balancing should generally be disabled.</td></tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="stopIrqBalance">
<label class="form-check-label" for="stopIrqBalance">
Stop IRQ Balancing
</label>
</td>
</tr>
<tr><td colspan="2">Network device budget (usec) controls how frequently the kernel passes batches of packets to the processing system. Low numbers tend to reduce latency, higher numbers can improve throughput.</td></tr>
<tr>
<td>Netdev Budget (usecs)</td>
<td><input type="number" min="0" max="1000000" id="netDevUsec" /></td>
</tr>
<tr><td colspan="2">Network device budget (packets) controls how frequently the kernel passes batches of packets to the processing system. Low numbers tend to reduce latency, higher numbers can improve throughput.</td></tr>
<tr>
<td>Netdev Budget (packets)</td>
<td><input type="number" min="0" max="1000000" id="netDevPackets" /></td>
</tr>
<tr><td colspan="2">How frequently should the kernel poll for receive packets?</td></tr>
<tr>
<td>RX Usecs</td>
<td><input type="number" min="0" max="1000000" id="rxUsecs" /></td>
</tr>
<tr><td colspan="2">How frequently should the kernel poll for transmit packets?</td></tr>
<tr>
<td>TX Usecs</td>
<td><input type="number" min="0" max="1000000" id="txUsecs" /></td>
</tr>
<tr><td colspan="2">If you are using VLANs, you generally need to enable this feature</td></tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="disableRxVlan">
<label class="form-check-label" for="disableRxVlan">
Disable RX VLAN Offloading
</label>
</td>
</tr>
<tr><td colspan="2">If you are using VLANs, you generally need to enable this feature</td></tr>
<tr>
<td colspan="2">
<input class="form-check-input" type="checkbox" value="" id="disableTxVlan">
<label class="form-check-label" for="disableTxVlan">
Disable TX VLAN Offloading
</label>
</td>
</tr>
<tr><td colspan="2">Offloads to disable. We've tried to include the important ones.</td></tr>
<tr>
<td>Disable Offloads (space separated)</td>
<td><input type="text" id="disableOffloadList" /></td>
</tr>
</table>
<p class="alert alert-info" role="alert">
At this time, you can only apply these settings to the current running instance. Edit <em>/etc/lqos</em> to
apply changes permanently. Applying tuning settings will not restart your XDP bridge.
</p>
<a class="btn btn-secondary" id="btnApplyTuning">Apply Tuning Settings</a>
</div>
<div class="tab-pane fade" id="v-pills-spylnx" role="tabpanel" aria-labelledby="v-pills-spylnx-tab">
Spylnx Settings
...
</div>
<div class="tab-pane fade" id="v-pills-uisp" role="tabpanel" aria-labelledby="v-pills-uisp-tab">
UISP Settings
...
</div>
<div class="tab-pane fade" id="v-pills-users" role="tabpanel" aria-labelledby="v-pills-users-tab">
<h2><i class="fa fa-users"></i> LibreQos Web Interface Users</h2>
<div id="userManager"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-2"></div>
</div>
</div>
<footer>Copyright (c) 2022, LibreQoE LLC</footer>
<script>
let python_config = null;
let nics = null;
let lqosd_config = null;
function start() {
display();
colorReloadButton();
updateHostCounts();
$.get("/api/admin_check", (is_admin) => {
if (!is_admin) {
$("#controls").html("<p class='alert alert-danger' role='alert'>You have to be an administrative user to change configuration.");
$("#userManager").html("<p class='alert alert-danger' role='alert'>Only administrators can see/change user information.");
} else {
// Handle Saving ispConfig.py
$("#btnSaveIspConfig").on('click', (data) => {
let new_config = python_config;
new_config.isp_interface = $("#nicCore").val();
new_config.internet_interface = $("#nicInternet").val();
new_config.on_a_stick_mode = $("#onAStick").prop('checked');
new_config.stick_vlans[0] = Number($("#StickVLANCore").val());
new_config.stick_vlans[1] = Number($("#StickVLANInternet").val());
new_config.sqm = $("#sqmMode").val();
new_config.total_download_mbps = Number($("#maxDownload").val());
new_config.total_upload_mbps = Number($("#maxUpload").val());
new_config.monitor_mode = $("#monitorMode").prop('checked');
new_config.generated_download_mbps = Number($("#generatedDownload").val());
new_config.generated_upload_mbps = Number($("#generatedUpload").val());
new_config.use_binpacking = $("#binpacking").prop('checked');
new_config.enable_shell_commands = $("#actualShellCommands").prop('checked');
new_config.run_as_sudo = $("#useSudo").prop('checked');
new_config.override_queue_count = Number($("#overrideQueues").val());
$.ajax({
type: "POST",
url: "/api/python_config",
data: JSON.stringify(new_config),
success: (data) => {
if (data == "ERROR") {
alert("Unable to create a first user.")
} else {
alert("Save Successful. Original backed up in ispConfig.py.backup. The window will now reload with the new configuration.");
window.location.reload()
}
}
})
});
}
$.get("/api/python_config", (data) => {
python_config = data;
$.get("/api/lqosd_config", (data) => {
lqosd_config = data;
$.get("/api/list_nics", (data) => {
nics = data;
fillNicList("nicCore", python_config.isp_interface);
fillNicList("nicInternet", python_config.internet_interface);
$("#onAStick").prop('checked', python_config.on_a_stick_mode);
$("#StickVLANCore").val(python_config.stick_vlans[0]);
$("#StickVLANInternet").val(python_config.stick_vlans[1]);
if (lqosd_config.bridge != null) {
$("#useKernelBridge").prop('checked', lqosd_config.bridge.use_kernel_bridge);
// Map Bifrost Interfaces
let html = "<h4>Interface Mapping</h4>";
html += "<table class='table'>";
html += "<thead><th>Input Interface</th><th>Output Interface</th><th>Scan VLANs?</th></thead>";
html += "<tbody>";
for (let i=0; i<lqosd_config.bridge.interface_mapping.length; i++) {
html += "<tr>";
html += "<td>" + buildNICList('bfIn_' + i, lqosd_config.bridge.interface_mapping[i].name, true) + "</td>";
html += "<td>" + buildNICList('bfOut_' + i, lqosd_config.bridge.interface_mapping[i].redirect_to, true) + "</td>";
html += "<td><input type='checkbox' class='form-check-input' id='bfScanVLAN_" + i + "'";
if (lqosd_config.bridge.interface_mapping[i].scan_vlans) {
html += ' checked';
}
html += " disabled='true' /></td>";
html += "</tr>";
}
html += "</tbody></table>";
$("#bifrostInterfaces").html(html);
// Map Bifrost VLAN mappings
html = "<h4>VLAN Mapping</h4>";
html += "<table class='table'>";
html += "<thead><th>Parent Interface</th><th>Input Tag</th><th>Remapped Tag</th></thead>";
html += "<tbody>";
for (let i=0; i<lqosd_config.bridge.vlan_mapping.length; i++) {
html += "<tr>";
html += "<td>" + buildNICList('bfvlanif_' + i, lqosd_config.bridge.vlan_mapping[i].parent, true) + "</td>";
html += "<td><input id='bfvlantag_" + i + "' type='number' min='0' max='4094' value='" + lqosd_config.bridge.vlan_mapping[i].tag + "' disabled='true' /></td>";
html += "<td><input id='bfvlanout_" + i + "' type='number' min='0' max='4094' value='" + lqosd_config.bridge.vlan_mapping[i].redirect_to + "' disabled='true' /></td>";
html += "</tr>";
}
html += "</tbody></table>";
$("#bifrostVlans").html(html);
}
$("#sqmMode option[value='" + python_config.sqm + "']").prop("selected", true);
$("#maxDownload").val(python_config.total_download_mbps);
$("#maxUpload").val(python_config.total_upload_mbps);
$("#monitorMode").prop('checked', python_config.monitor_mode);
$("#generatedDownload").val(python_config.generated_download_mbps);
$("#generatedUpload").val(python_config.generated_upload_mbps);
$("#binpacking").prop('checked', python_config.use_binpacking);
$("#queuecheckms").val(lqosd_config.queue_check_period_ms);
$("#actualShellCommands").prop('checked', python_config.enable_shell_commands);
$("#useSudo").prop('checked', python_config.run_as_sudo);
$("#overrideQueues").val(python_config.override_queue_count);
$("#stopIrqBalance").prop('checked', lqosd_config.tuning.stop_irq_balance);
$("#netDevUsec").val(lqosd_config.tuning.netdev_budget_usecs);
$("#netDevPackets").val(lqosd_config.tuning.netdev_budget_packets);
$("#rxUsecs").val(lqosd_config.tuning.rx_usecs);
$("#txUsecs").val(lqosd_config.tuning.tx_usecs);
$("#disableRxVlan").prop('checked', lqosd_config.tuning.disable_rxvlan);
$("#disableTxVlan").prop('checked', lqosd_config.tuning.disable_txvlan);
let offloads = "";
for (let i=0; i<lqosd_config.tuning.disable_offload.length; i++) {
offloads += lqosd_config.tuning.disable_offload[i] + " ";
}
$("#disableOffloadList").val(offloads);
// User management
if (is_admin) {
userManager();
tuning();
}
});
});
});
});
}
function userManager() {
let html = "<p>For now, please use <em>bin/webusers</em> to manage users.</p>";
$("#userManager").html(html);
}
function tuning() {
$("#btnApplyTuning").on('click', () => {
let period = Number($("#queuecheckms").val());
let new_config = {
stop_irq_balance: $("#stopIrqBalance").prop('checked'),
netdev_budget_usecs: Number($("#netDevUsec").val()),
netdev_budget_packets: Number($("#netDevPackets").val()),
rx_usecs: Number($("#rxUsecs").val()),
tx_usecs: Number($("#txUsecs").val()),
disable_rxvlan: $("#disableRxVlan").prop('checked'),
disable_txvlan: $("#disableTxVlan").prop('checked'),
disable_offload: $("#disableOffloadList").val().split(' ')
};
$.ajax({
type: "POST",
url: "/api/lqos_tuning/" + period,
data: JSON.stringify(new_config),
success: (data) => {
if (data == "ERROR") {
alert("Unable to apply settings.")
} else {
alert("Settings Applied");
}
}
})
});
}
function fillNicList(id, selected) {
let select = $("#" + id);
let html = "";
for (i=0; i<nics.length; i++) {
html += "<option value=\"";
html += nics[i][0] + "\"";
if (nics[i][0] == selected) {
html += " selected";
}
html += ">" + nics[i][0] + " - " + nics[i][1] + " - " + nics[i][2] + "</option>";
}
select.html(html);
}
function buildNICList(id, selected, disabled=false) {
let html = "<select id='" + id + "'";
if (disabled) html += " disabled='true' ";
html += ">";
for (i=0; i<nics.length; i++) {
html += "<option value=\"";
html += nics[i][0] + "\"";
if (nics[i][0] == selected) {
html += " selected";
}
html += ">" + nics[i][0] + " - " + nics[i][1] + " - " + nics[i][2] + "</option>";
}
html += "</select>";
return html;
}
function display() {
let colorPreference = window.localStorage.getItem("colorPreference");
if (colorPreference == null) {
window.localStorage.setItem("colorPreference", 0);
colorPreference = 0;
}
$("#colorMode option[id='" + colorPreference + "']").attr("selected", true);
let redact = window.localStorage.getItem("redact");
if (redact == null) {
window.localStorage.setItem("redact", false);
redact = false;
}
if (redact == "false") redact = false;
$("#redact").prop('checked', redact);
$("#applyDisplay").on('click', () => {
let colorPreference = $("#colorMode").find('option:selected').attr('id');
window.localStorage.setItem("colorPreference", colorPreference);
let redact = $("#redact").prop('checked');
window.localStorage.setItem("redact", redact);
});
}
$(document).ready(start);
</script>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

View File

@@ -0,0 +1,93 @@
<!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">
<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>
</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>webusers.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/webusers" 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>Copyright (c) 2022, 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>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,73 @@
<!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">
<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>
</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/webusers</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>Copyright (c) 2022, LibreQoE LLC</footer>
<script>
function start() {
$("#btnLogin").on('click', (data) => {
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 = "/";
}
}
})
});
}
$(document).ready(start);
</script>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,18 @@
@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; }
.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; }

View File

@@ -0,0 +1,188 @@
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() {
$.get("/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);
});
}
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;
}
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>`;

View File

@@ -0,0 +1,340 @@
<!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">
<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>
</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" aria-current="page" href="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</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 ms-auto">
<a class="nav-link" href="/config"><i class="fa fa-gear"></i> Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" id="startTest"><i class="fa fa-flag-checkered"></i> Run Bandwidth Test</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">
<!-- 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</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">
<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-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>
<!-- Idle/Activity Quantiles -->
<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> Utilization Quantiles</h5>
<div id="capacityHistogram" 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</h5>
<div id="top10dl"></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 RTT</h5>
<div id="worstRtt"></div>
</div>
</div>
</div>
</div>
</div>
<footer>Copyright (c) 2022, LibreQoE LLC</footer>
<script>
function updateCurrentThroughput() {
$.get("/api/current_throughput", (tp) => {
$("#ppsDown").text(scaleNumber(tp.packets_per_second[0]));
$("#ppsUp").text(scaleNumber(tp.packets_per_second[1]));
$("#bpsDown").text(scaleNumber(tp.bits_per_second[0]));
$("#bpsUp").text(scaleNumber(tp.bits_per_second[1]));
setTimeout(updateCurrentThroughput, 1000);
});
}
function updateThroughputGraph() {
$.get("/api/throughput_ring", (tp) => {
let graph = document.getElementById("tpGraph");
let x = [];
let y = []; // Down
let y2 = []; // Up
let y3 = []; // Shaped Down
let y4 = []; // Shaped Up
for (i=0; i<300; i++) {
x.push(i);
y.push(tp[i].bits_per_second[0]);
y2.push(0.0 - tp[i].bits_per_second[1]);
y3.push(tp[i].shaped_bits_per_second[0]);
y4.push(0.0 - tp[i].shaped_bits_per_second[1]);
}
let data = [
{x: x, y:y, name: 'Download', type: 'scatter'},
{x: x, y:y2, name: 'Upload', type: 'scatter'},
{x: x, y:y3, name: 'Shaped Download', type: 'scatter', fill: 'tozeroy'},
{x: x, y:y4, name: 'Shaped Upload', type: 'scatter', fill: 'tozeroy'},
];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true }, xaxis: {automargin: true, title: "Time since now (seconds)"} }, { responsive: true });
//console.log(tp);
setTimeout(updateThroughputGraph, 1000);
});
}
function updateThroughputQuantile() {
$.get("/api/busy_quantile", (tp) => {
//console.log(tp);
let graph = document.getElementById("capacityHistogram");
let x1 = [];
let x2 = [];
let y1 = [];
let y2 = [];
for (let i=0; i<10; i++) {
x1.push(i*10);
x2.push(i*10);
if (i > 0) {
y1.push(tp[i][0]);
y2.push(tp[i][1]);
} else {
y1.push(0);
y2.push(0);
}
}
let data = [
{x: x1, y:y1, type: 'bar', name: 'Download'},
{x: x1, y:y2, type: 'bar', name: 'Upload'},
];
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:0,t:0,pad:4 }, yaxis: { automargin: true, title: '# Samples' }, xaxis: {automargin: true, title: "% utilization"} }, { responsive: true });
setTimeout(updateThroughputQuantile, 1000);
});
}
function updateCpu() {
$.get("/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 });
setTimeout(updateCpu, 2000);
});
}
function updateRam() {
$.get("/api/ram", (ram) => {
let graph = document.getElementById("ram");
let data = [ {
values: [ram[0], 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 });
setTimeout(updateRam, 30000);
});
}
function updateNTable(target, tt) {
let html = "<table class='table'>";
html += "<thead><th>IP Address</th><th>DL ⬇️</th><th>UL ⬆️</th><th>RTT (ms)</th><th>Shaped</th></thead>";
for (let i=0; i<tt.length; i++) {
let color = color_ramp(tt[i].median_tcp_rtt);
html += "<tr style='background-color: " + color + "'>";
if (tt[i].circuit_id != "") {
html += "<td><a class='redact' href='/circuit_queue?id=" + encodeURI(tt[i].circuit_id) + "'>" + redactText(tt[i].ip_address) + "</td>";
} else {
html += "<td>" + tt[i].ip_address + "</td>";
}
html += "<td>" + scaleNumber(tt[i].bits_per_second[0]) + "</td>";
html += "<td>" + scaleNumber(tt[i].bits_per_second[1]) + "</td>";
html += "<td>" + tt[i].median_tcp_rtt.toFixed(2) + "</td>";
if (tt[i].tc_handle !=0) {
html += "<td><i class='fa fa-check-circle'></i> (" + tt[i].plan[0] + "/" + tt[i].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 += "</tr>";
}
html += "</table>";
$(target).html(html);
}
function updateTop10() {
$.get("/api/top_10_downloaders", (tt) => {
updateNTable('#top10dl', tt);
setTimeout(updateTop10, 5000);
});
}
function updateWorst10() {
$.get("/api/worst_10_rtt", (tt) => {
updateNTable('#worstRtt', tt);
setTimeout(updateWorst10, 5000);
});
}
function updateHistogram() {
$.get("/api/rtt_histogram", (rtt) => {
let graph = document.getElementById("rttHistogram");
let x = [];
let y = [];
for (let i=0; i<rtt.length; i++) {
x.push(i*10.0);
y.push(rtt[i]);
}
let data = [
{x:x, y:y, type: 'bar', marker: { color:x, colorscale: 'RdBu' } }
]
Plotly.newPlot(graph, data, { margin: { l:0,r:0,b:35,t:0 }, xaxis: { title: 'TCP Round-Trip Time (ms)' } }, { responsive: true });
setTimeout(updateHistogram, 5000);
});
}
function start() {
colorReloadButton();
updateCurrentThroughput();
updateThroughputGraph();
updateCpu();
updateRam();
updateTop10();
updateWorst10();
updateHistogram();
updateHostCounts();
updateThroughputQuantile();
$("#startTest").on('click', () => {
$.get("/api/run_btest", () => {});
});
}
$(document).ready(start);
</script>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,170 @@
<!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">
<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>
</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="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</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 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>Copyright (c) 2022, 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>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,166 @@
<!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">
<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>
</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="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</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 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> 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>Copyright (c) 2022, 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_min_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() {
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>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<?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>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,126 @@
<!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">
<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>
</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="/"><i class="fa fa-home"></i> Dashboard</a>
</li>
<li class="nav-item" id="currentLogin"></li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fa fa-globe"></i> Network Layout</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 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-address-card"></i> Unmapped IP Addresses (Most recently seen first)</h5>
<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>Copyright (c) 2022, 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 += "</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);
});
}
$(document).ready(start);
</script>
<!-- Leave to last -->
<script src="vendor/bootstrap.bundle.min.js"></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

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
[package]
name = "lqos_python"
version = "0.1.0"
edition = "2021"
[lib]
name = "lqos_python"
crate-type = ["cdylib"]
[dependencies]
pyo3 = "0.17"
lqos_bus = { path = "../lqos_bus" }
tokio = { version = "1.22", features = [ "rt", "macros", "net", "io-util" ] }
anyhow = "1"

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
use lqos_bus::{
decode_response, encode_request, BusRequest, BusResponse, BusSession, BUS_BIND_ADDRESS,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
};
pub fn run_query(requests: Vec<BusRequest>) -> Result<Vec<BusResponse>> {
let mut replies = Vec::new();
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let mut stream = TcpStream::connect(BUS_BIND_ADDRESS).await?;
let test = BusSession {
auth_cookie: 1234,
requests: requests,
};
let msg = encode_request(&test)?;
stream.write(&msg).await?;
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf).await?;
let reply = decode_response(&buf)?;
replies.extend_from_slice(&reply.responses);
Ok(replies)
})
}

View File

@@ -0,0 +1,122 @@
use lqos_bus::{BusRequest, BusResponse, TcHandle};
use pyo3::{
exceptions::PyOSError, pyclass, pyfunction, pymodule, types::PyModule, wrap_pyfunction,
PyResult, Python,
};
mod blocking;
use anyhow::{Error, Result};
use blocking::run_query;
/// Defines the Python module exports.
/// All exported functions have to be listed here.
#[pymodule]
fn liblqos_python(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PyIpMapping>()?;
m.add_wrapped(wrap_pyfunction!(is_lqosd_alive))?;
m.add_wrapped(wrap_pyfunction!(list_ip_mappings))?;
m.add_wrapped(wrap_pyfunction!(clear_ip_mappings))?;
m.add_wrapped(wrap_pyfunction!(delete_ip_mapping))?;
m.add_wrapped(wrap_pyfunction!(add_ip_mapping))?;
Ok(())
}
/// Check that `lqosd` is running.
///
/// Returns true if it is running, false otherwise.
#[pyfunction]
fn is_lqosd_alive(_py: Python) -> PyResult<bool> {
if let Ok(reply) = run_query(vec![BusRequest::Ping]) {
for resp in reply.iter() {
match resp {
BusResponse::Ack => return Ok(true),
_ => {}
}
}
}
Ok(false)
}
/// Provides a representation of an IP address mapping
/// Available through python by field name.
#[pyclass]
pub struct PyIpMapping {
#[pyo3(get)]
pub ip_address: String,
#[pyo3(get)]
pub prefix_length: u32,
#[pyo3(get)]
pub tc_handle: (u16, u16),
#[pyo3(get)]
pub cpu: u32,
}
/// Returns a list of all IP mappings
#[pyfunction]
fn list_ip_mappings(_py: Python) -> PyResult<Vec<PyIpMapping>> {
let mut result = Vec::new();
if let Ok(reply) = run_query(vec![BusRequest::ListIpFlow]) {
for resp in reply.iter() {
match resp {
BusResponse::MappedIps(map) => {
for mapping in map.iter() {
result.push(PyIpMapping {
ip_address: mapping.ip_address.clone(),
prefix_length: mapping.prefix_length,
tc_handle: mapping.tc_handle.get_major_minor(),
cpu: mapping.cpu,
});
}
}
_ => {}
}
}
}
Ok(result)
}
/// Clear all IP address to TC/CPU mappings
#[pyfunction]
fn clear_ip_mappings(_py: Python) -> PyResult<()> {
run_query(vec![BusRequest::ClearIpFlow]).unwrap();
Ok(())
}
/// Deletes an IP to CPU/TC mapping.
///
/// ## Arguments
///
/// * `ip_address`: The IP address to unmap.
/// * `upload`: `true` if this needs to be applied to the upload map (for a split/stick setup)
#[pyfunction]
fn delete_ip_mapping(_py: Python, ip_address: String, upload: bool) -> PyResult<()> {
run_query(vec![BusRequest::DelIpFlow { ip_address, upload }]).unwrap();
Ok(())
}
/// Internal function
/// Converts IP address arguments into an IP mapping request.
fn parse_add_ip(ip: &str, classid: &str, cpu: &str, upload: bool) -> Result<BusRequest> {
if !classid.contains(":") {
return Err(Error::msg(
"Class id must be in the format (major):(minor), e.g. 1:12",
));
}
Ok(BusRequest::MapIpToFlow {
ip_address: ip.to_string(),
tc_handle: TcHandle::from_string(classid)?,
cpu: u32::from_str_radix(&cpu.replace("0x", ""), 16)?, // Force HEX representation
upload,
})
}
/// Adds an IP address mapping
#[pyfunction]
fn add_ip_mapping(ip: String, classid: String, cpu: String, upload: bool) -> PyResult<()> {
let request = parse_add_ip(&ip, &classid, &cpu, upload);
if let Ok(request) = request {
run_query(vec![request]).unwrap();
Ok(())
} else {
Err(PyOSError::new_err(request.err().unwrap().to_string()))
}
}

View File

@@ -0,0 +1,15 @@
[package]
name = "lqos_sys"
version = "0.1.0"
edition = "2021"
[dependencies]
nix = "0.25"
libbpf-sys = "1"
anyhow = "1"
byteorder = "1.4"
lqos_bus = { path = "../lqos_bus" }
lqos_config = { path = "../lqos_config" }
[build-dependencies]
bindgen = "0.53.1"

View File

@@ -0,0 +1,7 @@
## lqos_sys
This crate wraps the XDP component in externally callable Rust. This is
used by other systems to manage the XDP/TC eBPF system.
The `src/bpf` directory contains the C for the eBPF program, as well as
some wrapper helpers to bring it into Rust-space.

146
src/rust/lqos_sys/build.rs Normal file
View File

@@ -0,0 +1,146 @@
use std::env;
use std::path::PathBuf;
use std::process::{Command, Output};
fn command_warnings(section: &str, command_result: &std::io::Result<Output>) {
if command_result.is_err() {
println!("cargo:warning=[{section}]{:?}", command_result);
}
let r = command_result.as_ref().unwrap().stdout.clone();
if !r.is_empty() {
println!("cargo:warning=[{section}]{}", String::from_utf8(r).unwrap());
}
let r = command_result.as_ref().unwrap().stderr.clone();
if !r.is_empty() {
panic!("{}", String::from_utf8(r).unwrap());
}
}
fn command_warnings_errors_only(section: &str, command_result: &std::io::Result<Output>) {
if command_result.is_err() {
println!("cargo:warning=[{section}]{:?}", command_result);
}
let r = command_result.as_ref().unwrap().stderr.clone();
if !r.is_empty() {
println!(
"cargo:warning=[{section}] {}",
String::from_utf8(r).unwrap()
);
}
}
fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap();
// 1: Shell out to build the lqos_bpf.ll XDP/TC combined program.
// Command line to wrap:
// clang -S -target bpf -Wall -Wno-unused-value -Wno-pointer-sign -Wno-compare-distinct-pointer-types -Werror -emit-llvm -c -g -I../headers/ -O2 -o "../bin"/libre_xdp_kern.ll libre_xdp_kern.c
let build_target = format!("{}/lqos_kern.ll", out_dir.to_str().unwrap());
let compile_result = Command::new("clang")
.current_dir("src/bpf")
.args([
"-S",
"-target",
"bpf",
"-Wall",
"-Wno-unused-value",
"-Wno-pointer-sign",
"-Wno-compare-distinct-pointer-types",
"-Werror",
"-emit-llvm",
"-c",
"-g",
"-O2",
"-o",
&build_target,
"lqos_kern.c",
])
.output();
command_warnings("clang", &compile_result);
// 2: Link the .ll file into a .o file
// Command line:
// llc -march=bpf -filetype=obj -o "../bin"/libre_xdp_kern.o "../bin"/libre_xdp_kern.ll
let link_target = format!("{}/lqos_kern.o", out_dir.to_str().unwrap());
let link_result = Command::new("llc")
.args([
"-march=bpf",
"-filetype=obj",
"-o",
&link_target,
&build_target,
])
.output();
command_warnings("llc", &link_result);
// 3: Use bpftool to build the skeleton file
// Command line:
// bpftool gen skeleton ../bin/libre_xdp_kern.o > libre_xdp_skel.h
let skel_target = format!("{}/lqos_kern_skel.h", out_dir.to_str().unwrap());
let skel_result = Command::new("bpftool")
.args(["gen", "skeleton", &link_target])
.output();
command_warnings_errors_only("bpf skel", &skel_result);
let header_file = String::from_utf8(skel_result.unwrap().stdout).unwrap();
std::fs::write(&skel_target, header_file).unwrap();
// 4: Copy the wrapper to our out dir
let wrapper_target = format!("{}/wrapper.h", out_dir.to_str().unwrap());
let wrapper_target_c = format!("{}/wrapper.c", out_dir.to_str().unwrap());
let shrinkwrap_lib = format!("{}/libshrinkwrap.o", out_dir.to_str().unwrap());
let shrinkwrap_a = format!("{}/libshrinkwrap.a", out_dir.to_str().unwrap());
std::fs::copy("src/bpf/wrapper.h", &wrapper_target).unwrap();
std::fs::copy("src/bpf/wrapper.c", &wrapper_target_c).unwrap();
// 5: Build the intermediary library
let build_result = Command::new("clang")
.current_dir("src/bpf")
.args([
"-c",
"wrapper.c",
&format!("-I{}", out_dir.to_str().unwrap()),
"-o",
&shrinkwrap_lib,
])
.output();
command_warnings("clang - wrapper", &build_result);
let _build_result = Command::new("ar")
.args([
"r",
&shrinkwrap_a,
&shrinkwrap_lib,
//"/usr/lib/x86_64-linux-gnu/libbpf.a",
])
.output();
//command_warnings(&build_result);
println!(
"cargo:rustc-link-search=native={}",
out_dir.to_str().unwrap()
);
println!("cargo:rustc-link-lib=static=shrinkwrap");
// 6: Use bindgen to generate a Rust wrapper
let bindings = bindgen::Builder::default()
// The input header we would like to generate
// bindings for.
.header(&wrapper_target)
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
// Finish the builder and generate the bindings.
.generate()
// Unwrap the Result and panic on failure.
.expect("Unable to generate bindings");
// Write the bindings to the $OUT_DIR/bindings.rs file.
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
}

View File

@@ -0,0 +1,70 @@
use anyhow::Result;
use lqos_config::{BridgeInterface, BridgeVlan};
use crate::{bpf_map::BpfMap, lqos_kernel::interface_name_to_index};
#[repr(C)]
#[derive(Default, Clone, Debug)]
struct BifrostInterface {
redirect_to: u32,
scan_vlans: u32,
}
#[repr(C)]
#[derive(Default, Clone, Debug)]
struct BifrostVlan {
redirect_to: u32,
}
const INTERFACE_PATH: &str = "/sys/fs/bpf/bifrost_interface_map";
const VLAN_PATH: &str = "/sys/fs/bpf/bifrost_vlan_map";
pub(crate) fn clear_bifrost() -> Result<()> {
println!("Clearing bifrost maps");
let mut interface_map = BpfMap::<u32, BifrostInterface>::from_path(INTERFACE_PATH)?;
let mut vlan_map = BpfMap::<u32, BifrostVlan>::from_path(VLAN_PATH)?;
println!("Clearing VLANs");
vlan_map.clear_no_repeat()?;
println!("Clearing Interfaces");
interface_map.clear_no_repeat()?;
println!("Done");
Ok(())
}
pub(crate) fn map_interfaces(mappings: &[BridgeInterface]) -> Result<()> {
println!("Interface maps");
let mut interface_map = BpfMap::<u32, BifrostInterface>::from_path(INTERFACE_PATH)?;
for mapping in mappings.iter() {
// Key is the parent interface
let mut from = interface_name_to_index(&mapping.name)?;
let redirect_to = interface_name_to_index(&mapping.redirect_to)?;
let mut mapping = BifrostInterface {
redirect_to,
scan_vlans: match mapping.scan_vlans {
true => 1,
false => 0,
},
};
interface_map.insert(&mut from, &mut mapping)?;
println!("Mapped bifrost interface {}->{}", from, redirect_to);
}
Ok(())
}
pub(crate) fn map_vlans(mappings: &[BridgeVlan]) -> Result<()> {
println!("VLAN maps");
let mut vlan_map = BpfMap::<u32, BifrostVlan>::from_path(VLAN_PATH)?;
for mapping in mappings.iter() {
let mut key: u32 = (interface_name_to_index(&mapping.parent)? << 16) | mapping.tag;
let mut val = BifrostVlan {
redirect_to: mapping.redirect_to,
};
vlan_map.insert(&mut key, &mut val)?;
println!(
"Mapped bifrost VLAN: {}:{} => {}",
mapping.parent, mapping.tag, mapping.redirect_to
);
println!("{key}");
}
Ok(())
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <stdbool.h>
#include "maximums.h"
#include "debug.h"
// Defines a bridge-free redirect interface.
struct bifrost_interface {
// The interface index to which this interface (from the key)
// should redirect.
__u32 redirect_to;
// Should VLANs be scanned (for VLAN redirection)?
// > 0 = true. 32-bit for padding reasons.
__u32 scan_vlans;
};
// Hash map defining up to 64 interface redirects.
// Keyed on the source interface index, value is a bifrost_interface
// structure.
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, struct bifrost_interface);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} bifrost_interface_map SEC(".maps");
// TODO: This could be a u32 if we don't need any additional info.
// Which VLAN should the keyed VLAN be redirected to?
struct bifrost_vlan {
__u32 redirect_to;
};
// Hash map of VLANs that should be redirected.
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, __u32);
__type(value, struct bifrost_vlan);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} bifrost_vlan_map SEC(".maps");

View File

@@ -0,0 +1,43 @@
#pragma once
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <stdbool.h>
#include "maximums.h"
#include "debug.h"
// Data structure used for map_txq_config.
// This is used to apply the queue_mapping in the TC part.
struct txq_config {
/* lookup key: __u32 cpu; */
__u16 queue_mapping;
__u16 htb_major;
};
/* Special map type that can XDP_REDIRECT frames to another CPU */
struct {
__uint(type, BPF_MAP_TYPE_CPUMAP);
__uint(max_entries, MAX_CPUS);
__type(key, __u32);
__type(value, __u32);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} cpu_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, MAX_CPUS);
__type(key, __u32);
__type(value, __u32);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} cpus_available SEC(".maps");
// Map used to store queue mappings
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, MAX_CPUS);
__type(key, __u32);
__type(value, struct txq_config);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} map_txq_config SEC(".maps");

View File

@@ -0,0 +1,13 @@
#pragma once
// Define VERBOSE if you want to fill
// `/sys/kernel/debug/tracing/trace_pipe` with per-packet debug
// info. You usually don't want this.
//#define VERBOSE 1
#define bpf_debug(fmt, ...) \
({ \
char ____fmt[] = " " fmt; \
bpf_trace_printk(____fmt, sizeof(____fmt), \
##__VA_ARGS__); \
})

View File

@@ -0,0 +1,290 @@
#pragma once
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <stdbool.h>
#include "../common/skb_safety.h"
#include "../common/debug.h"
#include "../common/ip_hash.h"
#include "../common/bifrost.h"
// Packet dissector for XDP. We don't have any help from Linux at this
// point.
struct dissector_t
{
// Pointer to the XDP context.
struct xdp_md *ctx;
// Start of data
void *start;
// End of data
void *end;
// Total length (end - start)
__u32 skb_len;
// Ethernet header once found (NULL until then)
struct ethhdr *ethernet_header;
// Ethernet packet type once found (0 until then)
__u16 eth_type;
// Layer-3 offset if found (0 until then)
__u32 l3offset;
// IPv4/6 header once found
union iph_ptr ip_header;
// Source IP address, encoded by `ip_hash.h`
struct in6_addr src_ip;
// Destination IP address, encoded by `ip_hash.h`
struct in6_addr dst_ip;
// Current VLAN tag. If there are multiple tags, it will be
// the INNER tag.
__be16 current_vlan;
};
// Representation of the VLAN header type.
struct vlan_hdr
{
// Tagged VLAN number
__be16 h_vlan_TCI;
// Protocol for the next section
__be16 h_vlan_encapsulated_proto;
};
// Representation of the PPPoE protocol header.
struct pppoe_proto
{
__u8 pppoe_version_type;
__u8 ppoe_code;
__be16 session_id;
__be16 pppoe_length;
__be16 proto;
};
#define PPPOE_SES_HLEN 8
#define PPP_IP 0x21
#define PPP_IPV6 0x57
// Representation of an MPLS label
struct mpls_label {
__be32 entry;
};
#define MPLS_LS_LABEL_MASK 0xFFFFF000
#define MPLS_LS_LABEL_SHIFT 12
#define MPLS_LS_TC_MASK 0x00000E00
#define MPLS_LS_TC_SHIFT 9
#define MPLS_LS_S_MASK 0x00000100
#define MPLS_LS_S_SHIFT 8
#define MPLS_LS_TTL_MASK 0x000000FF
#define MPLS_LS_TTL_SHIFT 0
// Constructor for a dissector
// Connects XDP/TC SKB structure to a dissector structure.
// Arguments:
// * ctx - an xdp_md structure, passed from the entry-point
// * dissector - pointer to a local dissector object to be initialized
//
// Returns TRUE if all is good, FALSE if the process cannot be completed
static __always_inline bool dissector_new(
struct xdp_md *ctx,
struct dissector_t *dissector
) {
dissector->ctx = ctx;
dissector->start = (void *)(long)ctx->data;
dissector->end = (void *)(long)ctx->data_end;
dissector->ethernet_header = (struct ethhdr *)NULL;
dissector->l3offset = 0;
dissector->skb_len = dissector->end - dissector->start;
dissector->current_vlan = 0;
// Check that there's room for an ethernet header
if SKB_OVERFLOW (dissector->start, dissector->end, ethhdr)
{
return false;
}
dissector->ethernet_header = (struct ethhdr *)dissector->start;
return true;
}
// Helper function - is an eth_type an IPv4 or v6 type?
static __always_inline bool is_ip(__u16 eth_type)
{
return eth_type == ETH_P_IP || eth_type == ETH_P_IPV6;
}
// Locates the layer-3 offset, if present. Fast returns for various
// common non-IP types. Will perform VLAN redirection if requested.
static __always_inline bool dissector_find_l3_offset(
struct dissector_t *dissector,
bool vlan_redirect
) {
if (dissector->ethernet_header == NULL)
{
bpf_debug("Ethernet header is NULL, still called offset check.");
return false;
}
__u32 offset = sizeof(struct ethhdr);
__u16 eth_type = bpf_ntohs(dissector->ethernet_header->h_proto);
// Fast return for unwrapped IP
if (eth_type == ETH_P_IP || eth_type == ETH_P_IPV6)
{
dissector->eth_type = eth_type;
dissector->l3offset = offset;
return true;
}
// Fast return for ARP or non-802.3 ether types
if (eth_type == ETH_P_ARP || eth_type < ETH_P_802_3_MIN)
{
return false;
}
// Walk the headers until we find IP
__u8 i = 0;
while (i < 10 && !is_ip(eth_type))
{
switch (eth_type)
{
// Read inside VLAN headers
case ETH_P_8021AD:
case ETH_P_8021Q:
{
if SKB_OVERFLOW_OFFSET (dissector->start, dissector->end,
offset, vlan_hdr)
{
return false;
}
struct vlan_hdr *vlan = (struct vlan_hdr *)
(dissector->start + offset);
dissector->current_vlan = vlan->h_vlan_TCI;
eth_type = bpf_ntohs(vlan->h_vlan_encapsulated_proto);
offset += sizeof(struct vlan_hdr);
// VLAN Redirection is requested, so lookup a detination and
// switch the VLAN tag if required
if (vlan_redirect) {
#ifdef VERBOSE
bpf_debug("Searching for redirect %u:%u",
dissector->ctx->ingress_ifindex,
bpf_ntohs(dissector->current_vlan)
);
#endif
__u32 key = (dissector->ctx->ingress_ifindex << 16) |
bpf_ntohs(dissector->current_vlan);
struct bifrost_vlan * vlan_info = NULL;
vlan_info = bpf_map_lookup_elem(&bifrost_vlan_map, &key);
if (vlan_info) {
#ifdef VERBOSE
bpf_debug("Redirect to VLAN %u",
bpf_htons(vlan_info->redirect_to)
);
#endif
vlan->h_vlan_TCI = bpf_htons(vlan_info->redirect_to);
}
}
}
break;
// Handle PPPoE
case ETH_P_PPP_SES:
{
if SKB_OVERFLOW_OFFSET (dissector->start, dissector->end,
offset, pppoe_proto)
{
return false;
}
struct pppoe_proto *pppoe = (struct pppoe_proto *)
(dissector->start + offset);
__u16 proto = bpf_ntohs(pppoe->proto);
switch (proto)
{
case PPP_IP:
eth_type = ETH_P_IP;
break;
case PPP_IPV6:
eth_type = ETH_P_IPV6;
break;
default:
return false;
}
offset += PPPOE_SES_HLEN;
}
break;
// WARNING/TODO: Here be dragons; this needs testing.
case ETH_P_MPLS_UC:
case ETH_P_MPLS_MC: {
if SKB_OVERFLOW_OFFSET(dissector->start, dissector-> end,
offset, mpls_label)
{
return false;
}
struct mpls_label * mpls = (struct mpls_label *)
(dissector->start + offset);
// Are we at the bottom of the stack?
offset += 4; // 32-bits
if (mpls->entry & MPLS_LS_S_MASK) {
// We've hit the bottom
if SKB_OVERFLOW_OFFSET(dissector->start, dissector->end,
offset, iphdr)
{
return false;
}
struct iphdr * iph = (struct iphdr *)(dissector->start + offset);
switch (iph->version) {
case 4: eth_type = ETH_P_IP; break;
case 6: eth_type = ETH_P_IPV6; break;
default: return false;
}
}
} break;
// We found something we don't know how to handle - bail out
default:
return false;
}
++i;
}
dissector->l3offset = offset;
dissector->eth_type = eth_type;
return true;
}
// Searches for an IP header.
static __always_inline bool dissector_find_ip_header(
struct dissector_t *dissector
) {
switch (dissector->eth_type)
{
case ETH_P_IP:
{
if (dissector->start + dissector->l3offset + sizeof(struct iphdr) >
dissector->end) {
return false;
}
dissector->ip_header.iph = dissector->start + dissector->l3offset;
if (dissector->ip_header.iph + 1 > dissector->end)
return false;
encode_ipv4(dissector->ip_header.iph->saddr, &dissector->src_ip);
encode_ipv4(dissector->ip_header.iph->daddr, &dissector->dst_ip);
return true;
}
break;
case ETH_P_IPV6:
{
if (dissector->start + dissector->l3offset +
sizeof(struct ipv6hdr) > dissector->end) {
return false;
}
dissector->ip_header.ip6h = dissector->start + dissector->l3offset;
if (dissector->ip_header.iph + 1 > dissector->end)
return false;
encode_ipv6(&dissector->ip_header.ip6h->saddr, &dissector->src_ip);
encode_ipv6(&dissector->ip_header.ip6h->daddr, &dissector->dst_ip);
return true;
}
break;
default:
return false;
}
}

View File

@@ -0,0 +1,223 @@
#pragma once
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <stdbool.h>
#include "../common/skb_safety.h"
#include "../common/debug.h"
#include "../common/ip_hash.h"
#include "dissector.h"
// Structure holding packet dissection information (obtained at the TC level)
struct tc_dissector_t
{
// Pointer to the SKB context.
struct __sk_buff *ctx;
// Pointer to the data start
void *start;
// Pointer to the data end
void *end;
// Pointer to the Ethernet header once obtained (NULL until then)
struct ethhdr *ethernet_header;
// Ethernet packet type, once obtained
__u16 eth_type;
// Start of layer-3 data, once obtained
__u32 l3offset;
// IP header (either v4 or v6), once obtained.
union iph_ptr ip_header;
// Source IP, encoded by `ip_hash.h` functions.
struct in6_addr src_ip;
// Destination IP, encoded by `ip_hash.h` functions.
struct in6_addr dst_ip;
// Current VLAN detected.
// TODO: This can probably be removed since the packet dissector
// now finds this.
__be16 current_vlan;
};
// Constructor for a dissector
// Connects XDP/TC SKB structure to a dissector structure.
// Arguments:
// * ctx - an xdp_md structure, passed from the entry-point
// * dissector - pointer to a local dissector object to be initialized
//
// Returns TRUE if all is good, FALSE if the process cannot be completed
static __always_inline bool tc_dissector_new(
struct __sk_buff *ctx,
struct tc_dissector_t *dissector
) {
dissector->ctx = ctx;
dissector->start = (void *)(long)ctx->data;
dissector->end = (void *)(long)ctx->data_end;
dissector->ethernet_header = (struct ethhdr *)NULL;
dissector->l3offset = 0;
dissector->current_vlan = bpf_htons(ctx->vlan_tci);
// Check that there's room for an ethernet header
if SKB_OVERFLOW (dissector->start, dissector->end, ethhdr)
{
return false;
}
dissector->ethernet_header = (struct ethhdr *)dissector->start;
return true;
}
// Search a context to find the layer-3 offset.
static __always_inline bool tc_dissector_find_l3_offset(
struct tc_dissector_t *dissector
) {
if (dissector->ethernet_header == NULL)
{
bpf_debug("Ethernet header is NULL, still called offset check.");
return false;
}
__u32 offset = sizeof(struct ethhdr);
__u16 eth_type = bpf_ntohs(dissector->ethernet_header->h_proto);
// Fast return for unwrapped IP
if (eth_type == ETH_P_IP || eth_type == ETH_P_IPV6)
{
dissector->eth_type = eth_type;
dissector->l3offset = offset;
return true;
}
// Fast return for ARP or non-802.3 ether types
if (eth_type == ETH_P_ARP || eth_type < ETH_P_802_3_MIN)
{
return false;
}
// Walk the headers until we find IP
__u8 i = 0;
while (i < 10 && !is_ip(eth_type))
{
switch (eth_type)
{
// Read inside VLAN headers
case ETH_P_8021AD:
case ETH_P_8021Q:
{
if SKB_OVERFLOW_OFFSET (dissector->start, dissector->end,
offset, vlan_hdr)
{
return false;
}
//bpf_debug("TC Found VLAN");
struct vlan_hdr *vlan = (struct vlan_hdr *)
(dissector->start + offset);
// Calculated from the SKB
//dissector->current_vlan = vlan->h_vlan_TCI;
eth_type = bpf_ntohs(vlan->h_vlan_encapsulated_proto);
offset += sizeof(struct vlan_hdr);
}
break;
// Handle PPPoE
case ETH_P_PPP_SES:
{
if SKB_OVERFLOW_OFFSET (dissector->start, dissector->end,
offset, pppoe_proto)
{
return false;
}
struct pppoe_proto *pppoe = (struct pppoe_proto *)
(dissector->start + offset);
__u16 proto = bpf_ntohs(pppoe->proto);
switch (proto)
{
case PPP_IP:
eth_type = ETH_P_IP;
break;
case PPP_IPV6:
eth_type = ETH_P_IPV6;
break;
default:
return false;
}
offset += PPPOE_SES_HLEN;
}
break;
// WARNING/TODO: Here be dragons; this needs testing.
case ETH_P_MPLS_UC:
case ETH_P_MPLS_MC: {
if SKB_OVERFLOW_OFFSET(dissector->start, dissector-> end,
offset, mpls_label)
{
return false;
}
struct mpls_label * mpls = (struct mpls_label *)
(dissector->start + offset);
// Are we at the bottom of the stack?
offset += 4; // 32-bits
if (mpls->entry & MPLS_LS_S_MASK) {
// We've hit the bottom
if SKB_OVERFLOW_OFFSET(dissector->start, dissector->end,
offset, iphdr)
{
return false;
}
struct iphdr * iph = (struct iphdr *)(dissector->start + offset);
switch (iph->version) {
case 4: eth_type = ETH_P_IP; break;
case 6: eth_type = ETH_P_IPV6; break;
default: return false;
}
}
} break;
// We found something we don't know how to handle - bail out
default:
return false;
}
++i;
}
dissector->l3offset = offset;
dissector->eth_type = eth_type;
return true;
}
// Locate the IP header if present
static __always_inline bool tc_dissector_find_ip_header(
struct tc_dissector_t *dissector
) {
switch (dissector->eth_type)
{
case ETH_P_IP:
{
if (dissector->start + dissector->l3offset +
sizeof(struct iphdr) > dissector->end) {
return false;
}
dissector->ip_header.iph = dissector->start + dissector->l3offset;
if (dissector->ip_header.iph + 1 > dissector->end) {
return false;
}
encode_ipv4(dissector->ip_header.iph->saddr, &dissector->src_ip);
encode_ipv4(dissector->ip_header.iph->daddr, &dissector->dst_ip);
return true;
}
break;
case ETH_P_IPV6:
{
if (dissector->start + dissector->l3offset +
sizeof(struct ipv6hdr) > dissector->end) {
return false;
}
dissector->ip_header.ip6h = dissector->start + dissector->l3offset;
if (dissector->ip_header.iph + 1 > dissector->end)
return false;
encode_ipv6(&dissector->ip_header.ip6h->saddr, &dissector->src_ip);
encode_ipv6(&dissector->ip_header.ip6h->daddr, &dissector->dst_ip);
return true;
}
break;
default:
return false;
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include <linux/in6.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
// Provides hashing services for merging IPv4 and IPv6 addresses into
// the same memory format.
// Union that contains either a pointer to an IPv4 header or an IPv6
// header. NULL if not present.
// Note that you also need to keep track of the header type, since
// accessing it directly without checking is undefined behavior.
union iph_ptr
{
// IPv4 Header
struct iphdr *iph;
// IPv6 Header
struct ipv6hdr *ip6h;
};
// Encodes an IPv4 address into an IPv6 address. All 0xFF except for the
// last 32-bits.
static __always_inline void encode_ipv4(
__be32 addr,
struct in6_addr * out_address
) {
__builtin_memset(&out_address->in6_u.u6_addr8, 0xFF, 16);
out_address->in6_u.u6_addr32[3] = addr;
}
// Encodes an IPv6 address into an IPv6 address. Unsurprisingly, that's
// just a memcpy operation.
static __always_inline void encode_ipv6(
struct in6_addr * ipv6_address,
struct in6_addr * out_address
) {
__builtin_memcpy(
&out_address->in6_u.u6_addr8,
&ipv6_address->in6_u.u6_addr8,
16
);
}

View File

@@ -0,0 +1,170 @@
#pragma once
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <stdbool.h>
#include <linux/in6.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include "maximums.h"
#include "debug.h"
#include "dissector.h"
#include "dissector_tc.h"
// Data structure used for map_ip_hash
struct ip_hash_info {
__u32 cpu;
__u32 tc_handle; // TC handle MAJOR:MINOR combined in __u32
};
// Key type used for map_ip_hash trie
struct ip_hash_key {
__u32 prefixlen; // Length of the prefix to match
struct in6_addr address; // An IPv6 address. IPv4 uses the last 32 bits.
};
// Map describing IP to CPU/TC mappings
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, IP_HASH_ENTRIES_MAX);
__type(key, struct ip_hash_key);
__type(value, struct ip_hash_info);
__uint(pinning, LIBBPF_PIN_BY_NAME);
__uint(map_flags, BPF_F_NO_PREALLOC);
} map_ip_to_cpu_and_tc SEC(".maps");
// RECIPROCAL Map describing IP to CPU/TC mappings
// If in "on a stick" mode, this is used to
// fetch the UPLOAD mapping.
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, IP_HASH_ENTRIES_MAX);
__type(key, struct ip_hash_key);
__type(value, struct ip_hash_info);
__uint(pinning, LIBBPF_PIN_BY_NAME);
__uint(map_flags, BPF_F_NO_PREALLOC);
} map_ip_to_cpu_and_tc_recip SEC(".maps");
// Performs an LPM lookup for an `ip_hash.h` encoded address, taking
// into account redirection and "on a stick" setup.
static __always_inline struct ip_hash_info * setup_lookup_key_and_tc_cpu(
// The "direction" constant from the main program. 1 = Internet,
// 2 = LAN, 3 = Figure it out from VLAN tags
int direction,
// Pointer to the "lookup key", which should contain the IP address
// to search for. Prefix length will be set for you.
struct ip_hash_key * lookup_key,
// Pointer to the traffic dissector.
struct dissector_t * dissector,
// Which VLAN represents the Internet, in redirection scenarios? (i.e.
// when direction == 3)
__be16 internet_vlan,
// Out variable setting the real "direction" of traffic when it has to
// be calculated.
int * out_effective_direction
)
{
lookup_key->prefixlen = 128;
// Normal preset 2-interface setup, no need to calculate any direction
// related VLANs.
if (direction < 3) {
lookup_key->address = (direction == 1) ? dissector->dst_ip :
dissector->src_ip;
*out_effective_direction = direction;
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc,
lookup_key
);
return ip_info;
} else {
if (dissector->current_vlan == internet_vlan) {
// Packet is coming IN from the Internet.
// Therefore it is download.
lookup_key->address = dissector->dst_ip;
*out_effective_direction = 1;
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc,
lookup_key
);
return ip_info;
} else {
// Packet is coming IN from the ISP.
// Therefore it is UPLOAD.
lookup_key->address = dissector->src_ip;
*out_effective_direction = 2;
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc_recip,
lookup_key
);
return ip_info;
}
}
}
// For the TC side, the dissector is different. Operates similarly to
// `setup_lookup_key_and_tc_cpu`. Performs an LPM lookup for an `ip_hash.h`
// encoded address, taking into account redirection and "on a stick" setup.
static __always_inline struct ip_hash_info * tc_setup_lookup_key_and_tc_cpu(
// The "direction" constant from the main program. 1 = Internet,
// 2 = LAN, 3 = Figure it out from VLAN tags
int direction,
// Pointer to the "lookup key", which should contain the IP address
// to search for. Prefix length will be set for you.
struct ip_hash_key * lookup_key,
// Pointer to the traffic dissector.
struct tc_dissector_t * dissector,
// Which VLAN represents the Internet, in redirection scenarios? (i.e.
// when direction == 3)
__be16 internet_vlan,
// Out variable setting the real "direction" of traffic when it has to
// be calculated.
int * out_effective_direction
)
{
lookup_key->prefixlen = 128;
// Direction is reversed because we are operating on egress
if (direction < 3) {
lookup_key->address = (direction == 1) ? dissector->src_ip :
dissector->dst_ip;
*out_effective_direction = direction;
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc,
lookup_key
);
return ip_info;
} else {
//bpf_debug("Current VLAN (TC): %d", dissector->current_vlan);
//bpf_debug("Source: %x", dissector->src_ip.in6_u.u6_addr32[3]);
//bpf_debug("Dest: %x", dissector->dst_ip.in6_u.u6_addr32[3]);
if (dissector->current_vlan == internet_vlan) {
// Packet is going OUT to the Internet.
// Therefore, it is UPLOAD.
lookup_key->address = dissector->src_ip;
*out_effective_direction = 2;
//bpf_debug("Reciprocal lookup");
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc_recip,
lookup_key
);
return ip_info;
} else {
// Packet is going OUT to the LAN.
// Therefore, it is DOWNLOAD.
lookup_key->address = dissector->dst_ip;
*out_effective_direction = 1;
//bpf_debug("Forward lookup");
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc,
lookup_key
);
return ip_info;
}
}
struct ip_hash_info * ip_info = bpf_map_lookup_elem(
&map_ip_to_cpu_and_tc,
lookup_key
);
return ip_info;
}

View File

@@ -0,0 +1,16 @@
#pragma once
// Maximum number of client IPs we are tracking
#define MAX_TRACKED_IPS 128000
// Maximum number of TC class mappings to support
#define IP_HASH_ENTRIES_MAX 128000
// Maximum number of supported CPUs
#define MAX_CPUS 1024
// Maximum number of TCP flows to track at once
#define MAX_FLOWS IP_HASH_ENTRIES_MAX*2
// Maximum number of packet pairs to track per flow.
#define MAX_PACKETS MAX_FLOWS

View File

@@ -0,0 +1,4 @@
#pragma once
#define SKB_OVERFLOW(start, end, T) ((void *)start + sizeof(struct T) > end)
#define SKB_OVERFLOW_OFFSET(start, end, offset, T) (start + offset + sizeof(struct T) > end)

View File

@@ -0,0 +1,79 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
Based on the GPLv2 xdp-pping project
(https://github.com/xdp-project/bpf-examples/tree/master/pping)
xdp_pping is based on the ideas in Dr. Kathleen Nichols' pping
utility: https://github.com/pollere/pping
and the papers around "Listening to Networks":
http://www.pollere.net/Pdfdocs/ListeningGoog.pdf
My modifications are Copyright 2022, Herbert Wolverson
(Bracket Productions)
*/
/* Shared structures between userspace and kernel space
*/
#ifndef __TC_CLASSIFY_KERN_PPING_COMMON_H
#define __TC_CLASSIFY_KERN_PPING_COMMON_H
/* 30 second rotating performance buffer, per-TC handle */
#define MAX_PERF_SECONDS 60
#define NS_PER_MS 1000000UL
#define NS_PER_MS_TIMES_100 10000UL
#define NS_PER_SECOND NS_PER_MS 1000000000UL
#define RECYCLE_RTT_INTERVAL 10000000000UL
/* Quick way to access a TC handle as either two 16-bit numbers or a single u32 */
union tc_handle_type
{
__u32 handle;
__u16 majmin[2];
};
/*
* Struct that can hold the source or destination address for a flow (l3+l4).
* Works for both IPv4 and IPv6, as IPv4 addresses can be mapped to IPv6 ones
* based on RFC 4291 Section 2.5.5.2.
*/
struct flow_address
{
struct in6_addr ip;
__u16 port;
__u16 reserved;
};
/*
* Struct to hold a full network tuple
* The ipv member is technically not necessary, but makes it easier to
* determine if saddr/daddr are IPv4 or IPv6 address (don't need to look at the
* first 12 bytes of address). The proto memeber is not currently used, but
* could be useful once pping is extended to work for other protocols than TCP.
*
* Note that I've removed proto, ipv and reserved.
*/
struct network_tuple
{
struct flow_address saddr;
struct flow_address daddr;
__u16 proto; // IPPROTO_TCP, IPPROTO_ICMP, QUIC etc
__u8 ipv; // AF_INET or AF_INET6
__u8 reserved;
};
/* Packet identifier */
struct packet_id
{
struct network_tuple flow;
__u32 identifier;
};
/* Ring-buffer of performance readings for each TC handle */
struct rotating_performance
{
__u32 rtt[MAX_PERF_SECONDS];
__u32 next_entry;
__u64 recycle_time;
__u32 has_fresh_data;
};
#endif /* __TC_CLASSIFY_KERN_PPING_COMMON_H */

View File

@@ -0,0 +1,777 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
Based on the GPLv2 xdp-pping project
(https://github.com/xdp-project/bpf-examples/tree/master/pping)
xdp_pping is based on the ideas in Dr. Kathleen Nichols' pping
utility: https://github.com/pollere/pping
and the papers around "Listening to Networks":
http://www.pollere.net/Pdfdocs/ListeningGoog.pdf
My modifications are Copyright 2022, Herbert Wolverson
(Bracket Productions)
*/
/* Shared structures between userspace and kernel space
*/
/* Implementation of pping inside the kernel
* classifier
*/
#ifndef __TC_CLASSIFY_KERN_PPING_H
#define __TC_CLASSIFY_KERN_PPING_H
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/pkt_cls.h>
#include <linux/in.h>
#include <linux/in6.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/tcp.h>
#include <bpf/bpf_endian.h>
#include <stdbool.h>
#include "tc_classify_kern_pping_common.h"
#include "maximums.h"
#include "debug.h"
#include "ip_hash.h"
#include "dissector_tc.h"
#define MAX_MEMCMP_SIZE 128
struct parsing_context
{
struct tcphdr *tcp;
__u64 now;
struct tc_dissector_t * dissector;
struct in6_addr * active_host;
};
/* Event type recorded for a packet flow */
enum __attribute__((__packed__)) flow_event_type
{
FLOW_EVENT_NONE,
FLOW_EVENT_OPENING,
FLOW_EVENT_CLOSING,
FLOW_EVENT_CLOSING_BOTH
};
enum __attribute__((__packed__)) connection_state
{
CONNECTION_STATE_EMPTY,
CONNECTION_STATE_WAITOPEN,
CONNECTION_STATE_OPEN,
CONNECTION_STATE_CLOSED
};
struct flow_state
{
__u64 last_timestamp;
__u32 last_id;
__u32 outstanding_timestamps;
enum connection_state conn_state;
__u8 reserved[2];
};
/*
* Stores flowstate for both direction (src -> dst and dst -> src) of a flow
*
* Uses two named members instead of array of size 2 to avoid hassels with
* convincing verifier that member access is not out of bounds
*/
struct dual_flow_state
{
struct flow_state dir1;
struct flow_state dir2;
};
/*
* Struct filled in by parse_packet_id.
*
* Note: As long as parse_packet_id is successful, the flow-parts of pid
* and reply_pid should be valid, regardless of value for pid_valid and
* reply_pid valid. The *pid_valid members are there to indicate that the
* identifier part of *pid are valid and can be used for timestamping/lookup.
* The reason for not keeping the flow parts as an entirely separate members
* is to save some performance by avoid doing a copy for lookup/insertion
* in the packet_ts map.
*/
struct packet_info
{
__u64 time; // Arrival time of packet
//__u32 payload; // Size of packet data (excluding headers)
struct packet_id pid; // flow + identifier to timestamp (ex. TSval)
struct packet_id reply_pid; // rev. flow + identifier to match against (ex. TSecr)
//__u32 ingress_ifindex; // Interface packet arrived on (if is_ingress, otherwise not valid)
bool pid_flow_is_dfkey; // Used to determine which member of dualflow state to use for forward direction
bool pid_valid; // identifier can be used to timestamp packet
bool reply_pid_valid; // reply_identifier can be used to match packet
enum flow_event_type event_type; // flow event triggered by packet
};
/*
* Struct filled in by protocol id parsers (ex. parse_tcp_identifier)
*/
struct protocol_info
{
__u32 pid;
__u32 reply_pid;
bool pid_valid;
bool reply_pid_valid;
enum flow_event_type event_type;
};
/* Map Definitions */
struct
{
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct packet_id);
__type(value, __u64);
__uint(max_entries, MAX_PACKETS);
__uint(pinning, LIBBPF_PIN_BY_NAME);
// __uint(map_flags, BPF_F_NO_PREALLOC);
} packet_ts SEC(".maps");
struct
{
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct network_tuple);
__type(value, struct dual_flow_state);
__uint(max_entries, MAX_FLOWS);
__uint(pinning, LIBBPF_PIN_BY_NAME);
// __uint(map_flags, BPF_F_NO_PREALLOC);
} flow_state SEC(".maps");
struct
{
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__type(key, struct in6_addr); // Keyed to the IP address
__type(value, struct rotating_performance);
__uint(max_entries, IP_HASH_ENTRIES_MAX);
__uint(pinning, LIBBPF_PIN_BY_NAME);
// __uint(map_flags, BPF_F_NO_PREALLOC);
} rtt_tracker SEC(".maps");
// Mask for IPv6 flowlabel + traffic class - used in fib lookup
#define IPV6_FLOWINFO_MASK __cpu_to_be32(0x0FFFFFFF)
#ifndef AF_INET
#define AF_INET 2
#endif
#ifndef AF_INET6
#define AF_INET6 10
#endif
#define MAX_TCP_OPTIONS 10
/* Functions */
/*
* Convenience function for getting the corresponding reverse flow.
* PPing needs to keep track of flow in both directions, and sometimes
* also needs to reverse the flow to report the "correct" (consistent
* with Kathie's PPing) src and dest address.
*/
static __always_inline void reverse_flow(
struct network_tuple *dest,
struct network_tuple *src
) {
dest->ipv = src->ipv;
dest->proto = src->proto;
dest->saddr = src->daddr;
dest->daddr = src->saddr;
dest->reserved = 0;
}
/*
* Can't seem to get __builtin_memcmp to work, so hacking my own
*
* Based on https://githubhot.com/repo/iovisor/bcc/issues/3559,
* __builtin_memcmp should work constant size but I still get the "failed to
* find BTF for extern" error.
*/
static __always_inline int my_memcmp(
const void *s1_,
const void *s2_,
__u32 size
) {
const __u8 *s1 = (const __u8 *)s1_, *s2 = (const __u8 *)s2_;
int i;
for (i = 0; i < MAX_MEMCMP_SIZE && i < size; i++)
{
if (s1[i] != s2[i])
return s1[i] > s2[i] ? 1 : -1;
}
return 0;
}
static __always_inline bool is_dualflow_key(struct network_tuple *flow)
{
return my_memcmp(&flow->saddr, &flow->daddr, sizeof(flow->saddr)) <= 0;
}
static __always_inline struct flow_state *fstate_from_dfkey(
struct dual_flow_state *df_state,
bool is_dfkey
) {
if (!df_state) {
return (struct flow_state *)NULL;
}
return is_dfkey ? &df_state->dir1 : &df_state->dir2;
}
/*
* Parses the TSval and TSecr values from the TCP options field. If sucessful
* the TSval and TSecr values will be stored at tsval and tsecr (in network
* byte order).
* Returns 0 if sucessful and -1 on failure
*/
static __always_inline int parse_tcp_ts(
struct tcphdr *tcph,
void *data_end,
__u32 *tsval,
__u32 *tsecr
) {
int len = tcph->doff << 2;
void *opt_end = (void *)tcph + len;
__u8 *pos = (__u8 *)(tcph + 1); // Current pos in TCP options
__u8 i, opt;
volatile __u8
opt_size; // Seems to ensure it's always read of from stack as u8
if (tcph + 1 > data_end || len <= sizeof(struct tcphdr))
return -1;
#pragma unroll // temporary solution until we can identify why the non-unrolled loop gets stuck in an infinite loop
for (i = 0; i < MAX_TCP_OPTIONS; i++)
{
if (pos + 1 > opt_end || pos + 1 > data_end)
return -1;
opt = *pos;
if (opt == 0) // Reached end of TCP options
return -1;
if (opt == 1)
{ // TCP NOP option - advance one byte
pos++;
continue;
}
// Option > 1, should have option size
if (pos + 2 > opt_end || pos + 2 > data_end)
return -1;
opt_size = *(pos + 1);
if (opt_size < 2) // Stop parsing options if opt_size has an invalid value
return -1;
// Option-kind is TCP timestap (yey!)
if (opt == 8 && opt_size == 10)
{
if (pos + 10 > opt_end || pos + 10 > data_end)
return -1;
*tsval = bpf_ntohl(*(__u32 *)(pos + 2));
*tsecr = bpf_ntohl(*(__u32 *)(pos + 6));
return 0;
}
// Some other TCP option - advance option-length bytes
pos += opt_size;
}
return -1;
}
/*
* Attempts to fetch an identifier for TCP packets, based on the TCP timestamp
* option.
*
* Will use the TSval as pid and TSecr as reply_pid, and the TCP source and dest
* as port numbers.
*
* If successful, tcph, sport, dport and proto_info will be set
* appropriately and 0 will be returned.
* On failure -1 will be returned (and arguments will not be set).
*/
static __always_inline int parse_tcp_identifier(
struct parsing_context *context,
__u16 *sport,
__u16 *dport,
struct protocol_info *proto_info
) {
if (parse_tcp_ts(context->tcp, context->dissector->end, &proto_info->pid,
&proto_info->reply_pid) < 0) {
return -1; // Possible TODO, fall back on seq/ack instead
}
// Do not timestamp pure ACKs (no payload)
void *nh_pos = (context->tcp + 1) + (context->tcp->doff << 2);
proto_info->pid_valid = nh_pos - context->dissector->start < context->dissector->ctx->len || context->tcp->syn;
// Do not match on non-ACKs (TSecr not valid)
proto_info->reply_pid_valid = context->tcp->ack;
// Check if connection is opening/closing
if (context->tcp->rst)
{
proto_info->event_type = FLOW_EVENT_CLOSING_BOTH;
}
else if (context->tcp->fin)
{
proto_info->event_type = FLOW_EVENT_CLOSING;
}
else if (context->tcp->syn)
{
proto_info->event_type = FLOW_EVENT_OPENING;
}
else
{
proto_info->event_type = FLOW_EVENT_NONE;
}
*sport = bpf_ntohs(context->tcp->dest);
*dport = bpf_ntohs(context->tcp->source);
return 0;
}
/* This is a bit of a hackjob from the original */
static __always_inline int parse_packet_identifier(
struct parsing_context *context,
struct packet_info *p_info
) {
p_info->time = context->now;
if (context->dissector->eth_type == ETH_P_IP)
{
p_info->pid.flow.ipv = AF_INET;
p_info->pid.flow.saddr.ip = context->dissector->src_ip;
p_info->pid.flow.daddr.ip = context->dissector->dst_ip;
}
else if (context->dissector->eth_type == ETH_P_IPV6)
{
p_info->pid.flow.ipv = AF_INET6;
p_info->pid.flow.saddr.ip = context->dissector->src_ip;
p_info->pid.flow.daddr.ip = context->dissector->dst_ip;
}
else
{
bpf_debug("Unknown protocol");
return -1;
}
//bpf_debug("IPs: %u %u", p_info->pid.flow.saddr.ip.in6_u.u6_addr32[3], p_info->pid.flow.daddr.ip.in6_u.u6_addr32[3]);
struct protocol_info proto_info;
int err = parse_tcp_identifier(context,
&p_info->pid.flow.saddr.port,
&p_info->pid.flow.daddr.port,
&proto_info);
if (err)
return -1;
//bpf_debug("Ports: %u %u", p_info->pid.flow.saddr.port, p_info->pid.flow.daddr.port);
// Sucessfully parsed packet identifier - fill in remaining members and return
p_info->pid.identifier = proto_info.pid;
p_info->pid_valid = proto_info.pid_valid;
p_info->reply_pid.identifier = proto_info.reply_pid;
p_info->reply_pid_valid = proto_info.reply_pid_valid;
p_info->event_type = proto_info.event_type;
if (p_info->pid.flow.ipv == AF_INET && p_info->pid.flow.ipv == AF_INET6) {
bpf_debug("Unknown internal protocol");
return -1;
}
p_info->pid_flow_is_dfkey = is_dualflow_key(&p_info->pid.flow);
reverse_flow(&p_info->reply_pid.flow, &p_info->pid.flow);
return 0;
}
static __always_inline struct network_tuple *
get_dualflow_key_from_packet(struct packet_info *p_info)
{
return p_info->pid_flow_is_dfkey ? &p_info->pid.flow : &p_info->reply_pid.flow;
}
/*
* Initilizes an "empty" flow state based on the forward direction of the
* current packet
*/
static __always_inline void init_flowstate(struct flow_state *f_state,
struct packet_info *p_info)
{
f_state->conn_state = CONNECTION_STATE_WAITOPEN;
f_state->last_timestamp = p_info->time;
}
static __always_inline void init_empty_flowstate(struct flow_state *f_state)
{
f_state->conn_state = CONNECTION_STATE_EMPTY;
}
static __always_inline struct flow_state *
get_flowstate_from_packet(struct dual_flow_state *df_state,
struct packet_info *p_info)
{
return fstate_from_dfkey(df_state, p_info->pid_flow_is_dfkey);
}
static __always_inline struct flow_state *
get_reverse_flowstate_from_packet(struct dual_flow_state *df_state,
struct packet_info *p_info)
{
return fstate_from_dfkey(df_state, !p_info->pid_flow_is_dfkey);
}
/*
* Initilize a new (assumed 0-initlized) dual flow state based on the current
* packet.
*/
static __always_inline void init_dualflow_state(
struct dual_flow_state *df_state,
struct packet_info *p_info
) {
struct flow_state *fw_state =
get_flowstate_from_packet(df_state, p_info);
struct flow_state *rev_state =
get_reverse_flowstate_from_packet(df_state, p_info);
init_flowstate(fw_state, p_info);
init_empty_flowstate(rev_state);
}
static __always_inline struct dual_flow_state *
create_dualflow_state(
struct parsing_context *ctx,
struct packet_info *p_info,
bool *new_flow
) {
struct network_tuple *key = get_dualflow_key_from_packet(p_info);
struct dual_flow_state new_state = {0};
init_dualflow_state(&new_state, p_info);
//new_state.dir1.tc_handle.handle = ctx->tc_handle;
//new_state.dir2.tc_handle.handle = ctx->tc_handle;
if (bpf_map_update_elem(&flow_state, key, &new_state, BPF_NOEXIST) ==
0)
{
if (new_flow)
*new_flow = true;
}
else
{
return (struct dual_flow_state *)NULL;
}
return (struct dual_flow_state *)bpf_map_lookup_elem(&flow_state, key);
}
static __always_inline struct dual_flow_state *
lookup_or_create_dualflow_state(
struct parsing_context *ctx,
struct packet_info *p_info,
bool *new_flow
) {
struct dual_flow_state *df_state;
struct network_tuple *key = get_dualflow_key_from_packet(p_info);
df_state = (struct dual_flow_state *)bpf_map_lookup_elem(&flow_state, key);
if (df_state)
{
return df_state;
}
// Only try to create new state if we have a valid pid
if (!p_info->pid_valid || p_info->event_type == FLOW_EVENT_CLOSING ||
p_info->event_type == FLOW_EVENT_CLOSING_BOTH)
return (struct dual_flow_state *)NULL;
return create_dualflow_state(ctx, p_info, new_flow);
}
static __always_inline bool is_flowstate_active(struct flow_state *f_state)
{
return f_state->conn_state != CONNECTION_STATE_EMPTY &&
f_state->conn_state != CONNECTION_STATE_CLOSED;
}
static __always_inline void update_forward_flowstate(
struct packet_info *p_info,
struct flow_state *f_state,
bool *new_flow
) {
// "Create" flowstate if it's empty
if (f_state->conn_state == CONNECTION_STATE_EMPTY &&
p_info->pid_valid)
{
init_flowstate(f_state, p_info);
if (new_flow)
*new_flow = true;
}
}
static __always_inline void update_reverse_flowstate(
void *ctx,
struct packet_info *p_info,
struct flow_state *f_state
) {
if (!is_flowstate_active(f_state))
return;
// First time we see reply for flow?
if (f_state->conn_state == CONNECTION_STATE_WAITOPEN &&
p_info->event_type != FLOW_EVENT_CLOSING_BOTH)
{
f_state->conn_state = CONNECTION_STATE_OPEN;
}
}
static __always_inline bool is_new_identifier(
struct packet_id *pid,
struct flow_state *f_state
) {
if (pid->flow.proto == IPPROTO_TCP)
/* TCP timestamps should be monotonically non-decreasing
* Check that pid > last_ts (considering wrap around) by
* checking 0 < pid - last_ts < 2^31 as specified by
* RFC7323 Section 5.2*/
return pid->identifier - f_state->last_id > 0 &&
pid->identifier - f_state->last_id < 1UL << 31;
return pid->identifier != f_state->last_id;
}
static __always_inline bool is_rate_limited(__u64 now, __u64 last_ts)
{
if (now < last_ts)
return true;
// Static rate limit
//return now - last_ts < DELAY_BETWEEN_RTT_REPORTS_MS * NS_PER_MS;
return false; // Max firehose drinking speed
}
/*
* Attempt to create a timestamp-entry for packet p_info for flow in f_state
*/
static __always_inline void pping_timestamp_packet(
struct flow_state *f_state,
void *ctx,
struct packet_info *p_info,
bool new_flow
) {
if (!is_flowstate_active(f_state) || !p_info->pid_valid)
return;
// Check if identfier is new
if (!new_flow && !is_new_identifier(&p_info->pid, f_state))
return;
f_state->last_id = p_info->pid.identifier;
// Check rate-limit
if (!new_flow && is_rate_limited(p_info->time, f_state->last_timestamp))
return;
/*
* Updates attempt at creating timestamp, even if creation of timestamp
* fails (due to map being full). This should make the competition for
* the next available map slot somewhat fairer between heavy and sparse
* flows.
*/
f_state->last_timestamp = p_info->time;
if (bpf_map_update_elem(&packet_ts, &p_info->pid, &p_info->time,
BPF_NOEXIST) == 0)
__sync_fetch_and_add(&f_state->outstanding_timestamps, 1);
}
/*
* Attempt to match packet in p_info with a timestamp from flow in f_state
*/
static __always_inline void pping_match_packet(struct flow_state *f_state,
struct packet_info *p_info,
struct in6_addr *active_host)
{
__u64 *p_ts;
if (!is_flowstate_active(f_state) || !p_info->reply_pid_valid)
return;
if (f_state->outstanding_timestamps == 0)
return;
p_ts = (__u64 *)bpf_map_lookup_elem(&packet_ts, &p_info->reply_pid);
if (!p_ts || p_info->time < *p_ts)
return;
__u64 rtt = (p_info->time - *p_ts) / NS_PER_MS_TIMES_100;
// Delete timestamp entry as soon as RTT is calculated
if (bpf_map_delete_elem(&packet_ts, &p_info->reply_pid) == 0)
{
__sync_fetch_and_add(&f_state->outstanding_timestamps, -1);
}
// Update the most performance map to include this data
struct rotating_performance *perf =
(struct rotating_performance *)bpf_map_lookup_elem(
&rtt_tracker, active_host);
if (perf == NULL) return;
__sync_fetch_and_add(&perf->next_entry, 1);
__u32 next_entry = perf->next_entry;
if (next_entry < MAX_PERF_SECONDS) {
__sync_fetch_and_add(&perf->rtt[next_entry], rtt);
perf->has_fresh_data = 1;
}
}
static __always_inline void close_and_delete_flows(
void *ctx,
struct packet_info *p_info,
struct flow_state *fw_flow,
struct flow_state *rev_flow
) {
// Forward flow closing
if (p_info->event_type == FLOW_EVENT_CLOSING ||
p_info->event_type == FLOW_EVENT_CLOSING_BOTH)
{
fw_flow->conn_state = CONNECTION_STATE_CLOSED;
}
// Reverse flow closing
if (p_info->event_type == FLOW_EVENT_CLOSING_BOTH)
{
rev_flow->conn_state = CONNECTION_STATE_CLOSED;
}
// Delete flowstate entry if neither flow is open anymore
if (!is_flowstate_active(fw_flow) && !is_flowstate_active(rev_flow))
{
bpf_map_delete_elem(&flow_state, get_dualflow_key_from_packet(p_info));
}
}
/*
* Contains the actual pping logic that is applied after a packet has been
* parsed and deemed to contain some valid identifier.
* Looks up and updates flowstate (in both directions), tries to save a
* timestamp of the packet, tries to match packet against previous timestamps,
* calculates RTTs and pushes messages to userspace as appropriate.
*/
static __always_inline void pping_parsed_packet(
struct parsing_context *context,
struct packet_info *p_info
) {
struct dual_flow_state *df_state;
struct flow_state *fw_flow, *rev_flow;
bool new_flow = false;
df_state = lookup_or_create_dualflow_state(context, p_info, &new_flow);
if (!df_state)
{
// bpf_debug("No flow state - stop");
return;
}
fw_flow = get_flowstate_from_packet(df_state, p_info);
update_forward_flowstate(p_info, fw_flow, &new_flow);
pping_timestamp_packet(fw_flow, context, p_info, new_flow);
rev_flow = get_reverse_flowstate_from_packet(df_state, p_info);
update_reverse_flowstate(context, p_info, rev_flow);
pping_match_packet(rev_flow, p_info, context->active_host);
close_and_delete_flows(context, p_info, fw_flow, rev_flow);
}
/* Entry poing for running pping in the tc context */
static __always_inline void tc_pping_start(struct parsing_context *context)
{
// Check to see if we can store perf info. Bail if we've hit the limit.
// Copying occurs because otherwise the validator complains.
struct rotating_performance *perf =
(struct rotating_performance *)bpf_map_lookup_elem(
&rtt_tracker, context->active_host);
if (perf) {
if (perf->next_entry >= MAX_PERF_SECONDS-1) {
//bpf_debug("Flow has max samples. Not sampling further until next reset.");
//for (int i=0; i<MAX_PERF_SECONDS; ++i) {
// bpf_debug("%u", perf->rtt[i]);
//}
if (context->now > perf->recycle_time) {
// If the time-to-live for the sample is exceeded, recycle it to be
// usable again.
//bpf_debug("Recycling flow, %u > %u", context->now, perf->recycle_time);
__builtin_memset(perf->rtt, 0, sizeof(__u32) * MAX_PERF_SECONDS);
perf->recycle_time = context->now + RECYCLE_RTT_INTERVAL;
perf->next_entry = 0;
perf->has_fresh_data = 0;
}
return;
}
}
// Populate the TCP Header
if (context->dissector->eth_type == ETH_P_IP)
{
// If its not TCP, stop
if (context->dissector->ip_header.iph + 1 > context->dissector->end)
return; // Stops the error checking from crashing
if (context->dissector->ip_header.iph->protocol != IPPROTO_TCP)
{
return;
}
context->tcp = (struct tcphdr *)((char *)context->dissector->ip_header.iph + (context->dissector->ip_header.iph->ihl * 4));
}
else if (context->dissector->eth_type == ETH_P_IPV6)
{
// If its not TCP, stop
if (context->dissector->ip_header.ip6h + 1 > context->dissector->end)
return; // Stops the error checking from crashing
if (context->dissector->ip_header.ip6h->nexthdr != IPPROTO_TCP)
{
return;
}
context->tcp = (struct tcphdr *)(context->dissector->ip_header.ip6h + 1);
}
else
{
bpf_debug("UNKNOWN PROTOCOL TYPE");
return;
}
// Bail out if the packet is incomplete
if (context->tcp + 1 > context->dissector->end)
{
return;
}
// If we didn't get a handle, make one
if (perf == NULL)
{
struct rotating_performance new_perf = {0};
new_perf.recycle_time = context->now + RECYCLE_RTT_INTERVAL;
new_perf.has_fresh_data = 0;
if (bpf_map_update_elem(&rtt_tracker, context->active_host, &new_perf, BPF_NOEXIST) != 0) return;
}
// Start the parsing process
struct packet_info p_info = {0};
if (parse_packet_identifier(context, &p_info) < 0)
{
//bpf_debug("Unable to parse packet identifier");
return;
}
pping_parsed_packet(context, &p_info);
}
#endif /* __TC_CLASSIFY_KERN_PPING_H */

View File

@@ -0,0 +1,70 @@
#pragma once
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/if_ether.h>
#include <stdbool.h>
#include "maximums.h"
#include "debug.h"
// Counter for each host
struct host_counter {
__u64 download_bytes;
__u64 upload_bytes;
__u64 download_packets;
__u64 upload_packets;
__u32 tc_handle;
};
// Pinned map storing counters per host. its an LRU structure: if it
// runs out of space, the least recently seen host will be removed.
struct
{
__uint(type, BPF_MAP_TYPE_LRU_PERCPU_HASH);
__type(key, struct in6_addr);
__type(value, struct host_counter);
__uint(max_entries, MAX_TRACKED_IPS);
__uint(pinning, LIBBPF_PIN_BY_NAME);
} map_traffic SEC(".maps");
static __always_inline void track_traffic(
int direction,
struct in6_addr * key,
__u32 size,
__u32 tc_handle
) {
// Count the bits. It's per-CPU, so we can't be interrupted - no sync required
struct host_counter * counter =
(struct host_counter *)bpf_map_lookup_elem(&map_traffic, key);
if (counter) {
if (direction == 1) {
// Download
counter->download_packets += 1;
counter->download_bytes += size;
counter->tc_handle = tc_handle;
} else {
// Upload
counter->upload_packets += 1;
counter->upload_bytes += size;
counter->tc_handle = tc_handle;
}
} else {
struct host_counter new_host = {0};
new_host.tc_handle = tc_handle;
if (direction == 1) {
new_host.download_packets = 1;
new_host.download_bytes = size;
new_host.upload_bytes = 0;
new_host.upload_packets = 0;
} else {
new_host.upload_packets = 1;
new_host.upload_bytes = size;
new_host.download_bytes = 0;
new_host.download_packets = 0;
}
if (bpf_map_update_elem(&map_traffic, key, &new_host, BPF_NOEXIST) != 0) {
bpf_debug("Failed to insert flow");
}
}
}

View File

@@ -0,0 +1,309 @@
/* SPDX-License-Identifier: GPL-2.0 */
// Minimal XDP program that passes all packets.
// Used to verify XDP functionality.
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/in6.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/pkt_cls.h>
#include <linux/pkt_sched.h> /* TC_H_MAJ + TC_H_MIN */
#include "common/debug.h"
#include "common/dissector.h"
#include "common/dissector_tc.h"
#include "common/maximums.h"
#include "common/throughput.h"
#include "common/lpm.h"
#include "common/cpu_map.h"
#include "common/tcp_rtt.h"
#include "common/bifrost.h"
/* Theory of operation:
1. (Packet arrives at interface)
2. XDP (ingress) starts
* Check that "direction" is set and any VLAN mappings
* Dissect the packet to find VLANs and L3 offset
* If VLAN redirection is enabled, change VLAN tags
* to swap ingress/egress VLANs.
* Perform LPM lookup to determine CPU destination
* Track traffic totals
* Perform CPU redirection
3. TC (ingress) starts
* If interface redirection is enabled, bypass the bridge
and redirect to the outbound interface.
* If VLAN redirection has happened, ONLY redirect if
there is a VLAN tag to avoid STP loops.
4. TC (egress) starts on the outbound interface
* LPM lookup to find TC handle
* If TCP, track RTT via ringbuffer and sampling
* Send TC redirect to track at the appropriate handle.
*/
// Constant passed in during loading to either
// 1 (facing the Internet)
// 2 (facing the LAN)
// 3 (use VLAN mode, we're running on a stick)
// If it stays at 255, we have a configuration error.
int direction = 255;
// Also configured during loading. For "on a stick" support,
// these are mapped to the respective VLAN facing directions.
__be16 internet_vlan = 0; // Note: turn these into big-endian
__be16 isp_vlan = 0;
// XDP Entry Point
SEC("xdp")
int xdp_prog(struct xdp_md *ctx)
{
#ifdef VERBOSE
bpf_debug("XDP-RDR");
#endif
if (direction == 255) {
bpf_debug("Error: interface direction unspecified, aborting.");
return XDP_PASS;
}
// Do we need to perform a VLAN redirect?
bool vlan_redirect = false;
{ // Note: scope for removing temporaries from the stack
__u32 my_interface = ctx->ingress_ifindex;
struct bifrost_interface * redirect_info = NULL;
redirect_info = bpf_map_lookup_elem(
&bifrost_interface_map,
&my_interface
);
if (redirect_info) {
// If we have a redirect, mark it - the dissector will
// apply it
vlan_redirect = true;
#ifdef VERBOSE
bpf_debug("(XDP) VLAN redirection requested for this interface");
#endif
}
}
struct dissector_t dissector = {0};
#ifdef VERBOSE
bpf_debug("(XDP) START XDP");
bpf_debug("(XDP) Running mode %u", direction);
bpf_debug("(XDP) Scan VLANs: %u %u", internet_vlan, isp_vlan);
#endif
// If the dissector is unable to figure out what's going on, bail
// out.
if (!dissector_new(ctx, &dissector)) return XDP_PASS;
// Note that this step rewrites the VLAN tag if redirection
// is requested.
if (!dissector_find_l3_offset(&dissector, vlan_redirect)) return XDP_PASS;
if (!dissector_find_ip_header(&dissector)) return XDP_PASS;
#ifdef VERBOSE
bpf_debug("(XDP) Spotted VLAN: %u", dissector.current_vlan);
#endif
// Determine the lookup key by direction
struct ip_hash_key lookup_key;
int effective_direction = 0;
struct ip_hash_info * ip_info = setup_lookup_key_and_tc_cpu(
direction,
&lookup_key,
&dissector,
internet_vlan,
&effective_direction
);
#ifdef VERBOSE
bpf_debug("(XDP) Effective direction: %d", effective_direction);
#endif
// Find the desired TC handle and CPU target
__u32 tc_handle = 0;
__u32 cpu = 0;
if (ip_info) {
tc_handle = ip_info->tc_handle;
cpu = ip_info->cpu;
}
// Update the traffic tracking buffers
track_traffic(
effective_direction,
&lookup_key.address,
ctx->data_end - ctx->data, // end - data = length
tc_handle
);
// Send on its way
if (tc_handle != 0) {
// Handle CPU redirection if there is one specified
__u32 *cpu_lookup;
cpu_lookup = bpf_map_lookup_elem(&cpus_available, &cpu);
if (!cpu_lookup) {
bpf_debug("Error: CPU %u is not mapped", cpu);
return XDP_PASS; // No CPU found
}
__u32 cpu_dest = *cpu_lookup;
// Redirect based on CPU
#ifdef VERBOSE
bpf_debug("(XDP) Zooming to CPU: %u", cpu_dest);
bpf_debug("(XDP) Mapped to handle: %u", tc_handle);
#endif
long redirect_result = bpf_redirect_map(&cpu_map, cpu_dest, 0);
#ifdef VERBOSE
bpf_debug("(XDP) Redirect result: %u", redirect_result);
#endif
return redirect_result;
}
return XDP_PASS;
}
// TC-Egress Entry Point
SEC("tc")
int tc_iphash_to_cpu(struct __sk_buff *skb)
{
#ifdef VERBOSE
bpf_debug("TC-MAP");
#endif
if (direction == 255) {
bpf_debug("(TC) Error: interface direction unspecified, aborting.");
return TC_ACT_OK;
}
#ifdef VERBOSE
bpf_debug("(TC) SKB VLAN TCI: %u", skb->vlan_tci);
#endif
__u32 cpu = bpf_get_smp_processor_id();
// Lookup the queue
{
struct txq_config *txq_cfg;
txq_cfg = bpf_map_lookup_elem(&map_txq_config, &cpu);
if (!txq_cfg) return TC_ACT_SHOT;
if (txq_cfg->queue_mapping != 0) {
skb->queue_mapping = txq_cfg->queue_mapping;
} else {
bpf_debug("(TC) Misconf: CPU:%u no conf (curr qm:%d)\n",
cpu, skb->queue_mapping);
}
} // Scope to remove tcq_cfg when done with it
// Once again parse the packet
// Note that we are returning OK on failure, which is a little odd.
// The reasoning being that if its a packet we don't know how to handle,
// we probably don't want to drop it - to ensure that IS-IS, ARP, STP
// and other packet types are still handled by the default queues.
struct tc_dissector_t dissector = {0};
if (!tc_dissector_new(skb, &dissector)) return TC_ACT_OK;
if (!tc_dissector_find_l3_offset(&dissector)) return TC_ACT_OK;
if (!tc_dissector_find_ip_header(&dissector)) return TC_ACT_OK;
// Determine the lookup key by direction
struct ip_hash_key lookup_key;
int effective_direction = 0;
struct ip_hash_info * ip_info = tc_setup_lookup_key_and_tc_cpu(
direction,
&lookup_key,
&dissector,
internet_vlan,
&effective_direction
);
#ifdef VERBOSE
bpf_debug("(TC) effective direction: %d", effective_direction);
#endif
// Call pping to obtain RTT times
struct parsing_context context = {0};
context.now = bpf_ktime_get_ns();
context.tcp = NULL;
context.dissector = &dissector;
context.active_host = &lookup_key.address;
tc_pping_start(&context);
if (ip_info && ip_info->tc_handle != 0) {
// We found a matching mapped TC flow
#ifdef VERBOSE
bpf_debug("(TC) Mapped to TC handle %x", ip_info->tc_handle);
#endif
skb->priority = ip_info->tc_handle;
return TC_ACT_OK;
} else {
// We didn't find anything
#ifdef VERBOSE
bpf_debug("(TC) didn't map anything");
#endif
return TC_ACT_OK;
}
return TC_ACT_OK;
}
// Helper function to call the bpf_redirect function and note
// errors from the TC-egress context.
static __always_inline long do_tc_redirect(__u32 target) {
//bpf_debug("Packet would have been redirected to ifindex %u", target);
//return TC_ACT_UNSPEC; // Don't actually redirect, we're testing
long ret = bpf_redirect(target, 0);
if (ret != TC_ACT_REDIRECT) {
bpf_debug("(TC-IN) TC Redirect call failed");
return TC_ACT_UNSPEC;
} else {
return ret;
}
}
// TC-Ingress entry-point. eBPF Bridge ("bifrost")
SEC("tc")
int bifrost(struct __sk_buff *skb)
{
#ifdef VERBOSE
bpf_debug("TC-Ingress invoked on interface: %u . %u",
skb->ifindex, skb->vlan_tci);
#endif
// Lookup to see if we have redirection setup
struct bifrost_interface * redirect_info = NULL;
__u32 my_interface = skb->ifindex;
redirect_info = bpf_map_lookup_elem(&bifrost_interface_map, &my_interface);
if (redirect_info) {
#ifdef VERBOSE
bpf_debug("(TC-IN) Redirect info: to: %u, scan vlans: %d",
redirect_info->redirect_to, redirect_info->scan_vlans);
#endif
if (redirect_info->scan_vlans) {
// We are in VLAN redirect mode. If VLAN redirection is required,
// it already happened in the XDP stage (rewriting the header).
//
// We need to ONLY redirect if we have tagged packets, otherwise
// we create STP loops and Bad Things (TM) happen.
if (skb->vlan_tci > 0) {
#ifdef VERBOSE
bpf_debug("(TC-IN) Redirecting back to same interface, \
VLAN %u", skb->vlan_tci);
#endif
return do_tc_redirect(redirect_info->redirect_to);
} else {
#ifdef VERBOSE
bpf_debug("(TC-IN) Not redirecting: No VLAN tag, bare \
redirect unsupported in VLAN mode.");
#endif
return TC_ACT_UNSPEC;
}
} else {
// We're in regular redirect mode. So if we aren't trying to send
// a packet out via the interface it arrived, we can redirect.
if (skb->ifindex == redirect_info->redirect_to) {
#ifdef VERBOSE
bpf_debug("(TC-IN) Not redirecting: src and dst are the \
same.");
#endif
return TC_ACT_UNSPEC;
} else {
return do_tc_redirect(redirect_info->redirect_to);
}
}
} else {
#ifdef VERBOSE
bpf_debug("(TC-IN) No matching redirect record for interface %u",
my_interface);
#endif
}
return TC_ACT_UNSPEC;
}
char _license[] SEC("license") = "GPL";

View File

@@ -0,0 +1,306 @@
#include "wrapper.h"
#include "common/maximums.h"
struct lqos_kern * lqos_kern_open() {
return lqos_kern__open();
}
int lqos_kern_load(struct lqos_kern * skel) {
return lqos_kern__load(skel);
}
extern __u64 max_tracker_ips() {
return MAX_TRACKED_IPS;
}
/////////////////////////////////////////////////////////////////////////////////////
// The following is derived from
// https://github.com/xdp-project/bpf-examples/blob/master/tc-policy/tc_txq_policy.c
// It needs converting to Rust, but I wanted to get something
// working relatively quickly.
#include <linux/bpf.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#define EGRESS_HANDLE 0x1;
#define EGRESS_PRIORITY 0xC02F;
int teardown_hook(int ifindex, const char * ifname, bool verbose)
{
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook,
.attach_point = BPF_TC_EGRESS,
.ifindex = ifindex);
int err;
/* When destroying the hook, any and ALL attached TC-BPF (filter)
* programs are also detached.
*/
err = bpf_tc_hook_destroy(&hook);
if (err)
fprintf(stderr, "Couldn't remove clsact qdisc on %s\n", ifname);
if (verbose)
printf("Flushed all TC-BPF egress programs (via destroy hook)\n");
return err;
}
int tc_detach_egress(int ifindex, bool verbose, bool flush_hook, const char * ifname)
{
int err;
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = ifindex,
.attach_point = BPF_TC_EGRESS);
DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts_info);
opts_info.handle = EGRESS_HANDLE;
opts_info.priority = EGRESS_PRIORITY;
/* Check what program we are removing */
err = bpf_tc_query(&hook, &opts_info);
if (err) {
fprintf(stderr, "No egress program to detach "
"for ifindex %d (err:%d)\n", ifindex, err);
return err;
}
if (verbose)
printf("Detaching TC-BPF prog id:%d\n", opts_info.prog_id);
/* Attempt to detach program */
opts_info.prog_fd = 0;
opts_info.prog_id = 0;
opts_info.flags = 0;
err = bpf_tc_detach(&hook, &opts_info);
if (err) {
fprintf(stderr, "Cannot detach TC-BPF program id:%d "
"for ifindex %d (err:%d)\n", opts_info.prog_id,
ifindex, err);
}
if (flush_hook)
return teardown_hook(ifindex, ifname, verbose);
return err;
}
int tc_attach_egress(int ifindex, bool verbose, struct lqos_kern *obj)
{
int err = 0;
int fd;
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .attach_point = BPF_TC_EGRESS);
DECLARE_LIBBPF_OPTS(bpf_tc_opts, attach_egress);
/* Selecting BPF-prog here: */
//fd = bpf_program__fd(obj->progs.queue_map_4);
fd = bpf_program__fd(obj->progs.tc_iphash_to_cpu);
if (fd < 0) {
fprintf(stderr, "Couldn't find egress program\n");
err = -ENOENT;
goto out;
}
attach_egress.prog_fd = fd;
hook.ifindex = ifindex;
err = bpf_tc_hook_create(&hook);
if (err && err != -EEXIST) {
fprintf(stderr, "Couldn't create TC-BPF hook for "
"ifindex %d (err:%d)\n", ifindex, err);
goto out;
}
if (verbose && err == -EEXIST) {
printf("Success: TC-BPF hook already existed "
"(Ignore: \"libbpf: Kernel error message\")\n");
}
hook.attach_point = BPF_TC_EGRESS;
attach_egress.flags = BPF_TC_F_REPLACE;
attach_egress.handle = EGRESS_HANDLE;
attach_egress.priority = EGRESS_PRIORITY;
err = bpf_tc_attach(&hook, &attach_egress);
if (err) {
fprintf(stderr, "Couldn't attach egress program to "
"ifindex %d (err:%d)\n", hook.ifindex, err);
goto out;
}
if (verbose) {
printf("Attached TC-BPF program id:%d\n",
attach_egress.prog_id);
}
out:
return err;
}
int teardown_hook_ingress(int ifindex, const char * ifname, bool verbose)
{
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook,
.attach_point = BPF_TC_INGRESS,
.ifindex = ifindex);
int err;
/* When destroying the hook, any and ALL attached TC-BPF (filter)
* programs are also detached.
*/
err = bpf_tc_hook_destroy(&hook);
if (err)
fprintf(stderr, "Couldn't remove clsact qdisc on %s\n", ifname);
if (verbose)
printf("Flushed all TC-BPF egress programs (via destroy hook)\n");
return err;
}
int tc_detach_ingress(int ifindex, bool verbose, bool flush_hook, const char * ifname)
{
int err;
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .ifindex = ifindex,
.attach_point = BPF_TC_INGRESS);
DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts_info);
opts_info.handle = EGRESS_HANDLE;
opts_info.priority = EGRESS_PRIORITY;
/* Check what program we are removing */
err = bpf_tc_query(&hook, &opts_info);
if (err) {
fprintf(stderr, "No ingress program to detach "
"for ifindex %d (err:%d)\n", ifindex, err);
return err;
}
if (verbose)
printf("Detaching TC-BPF prog id:%d\n", opts_info.prog_id);
/* Attempt to detach program */
opts_info.prog_fd = 0;
opts_info.prog_id = 0;
opts_info.flags = 0;
err = bpf_tc_detach(&hook, &opts_info);
if (err) {
fprintf(stderr, "Cannot detach TC-BPF program id:%d "
"for ifindex %d (err:%d)\n", opts_info.prog_id,
ifindex, err);
}
if (flush_hook)
return teardown_hook(ifindex, ifname, verbose);
return err;
}
int tc_attach_ingress(int ifindex, bool verbose, struct lqos_kern *obj)
{
int err = 0;
int fd;
DECLARE_LIBBPF_OPTS(bpf_tc_hook, hook, .attach_point = BPF_TC_INGRESS);
DECLARE_LIBBPF_OPTS(bpf_tc_opts, attach_egress);
/* Selecting BPF-prog here: */
//fd = bpf_program__fd(obj->progs.queue_map_4);
fd = bpf_program__fd(obj->progs.bifrost);
if (fd < 0) {
fprintf(stderr, "Couldn't find ingress program\n");
err = -ENOENT;
goto out;
}
attach_egress.prog_fd = fd;
hook.ifindex = ifindex;
err = bpf_tc_hook_create(&hook);
if (err && err != -EEXIST) {
fprintf(stderr, "Couldn't create TC-BPF hook for "
"ifindex %d (err:%d)\n", ifindex, err);
goto out;
}
if (verbose && err == -EEXIST) {
printf("Success: TC-BPF hook already existed "
"(Ignore: \"libbpf: Kernel error message\")\n");
}
hook.attach_point = BPF_TC_INGRESS;
attach_egress.flags = BPF_TC_F_REPLACE;
attach_egress.handle = EGRESS_HANDLE;
attach_egress.priority = EGRESS_PRIORITY;
err = bpf_tc_attach(&hook, &attach_egress);
if (err) {
fprintf(stderr, "Couldn't attach egress program to "
"ifindex %d (err:%d)\n", hook.ifindex, err);
goto out;
}
if (verbose) {
printf("Attached TC-BPF program id:%d\n",
attach_egress.prog_id);
}
out:
return err;
}
/*******************************/
static inline unsigned int bpf_num_possible_cpus(void)
{
static const char *fcpu = "/sys/devices/system/cpu/possible";
unsigned int start, end, possible_cpus = 0;
char buff[128];
FILE *fp;
int n;
fp = fopen(fcpu, "r");
if (!fp) {
printf("Failed to open %s: '%s'!\n", fcpu, strerror(errno));
exit(1);
}
while (fgets(buff, sizeof(buff), fp)) {
n = sscanf(buff, "%u-%u", &start, &end);
if (n == 0) {
printf("Failed to retrieve # possible CPUs!\n");
exit(1);
} else if (n == 1) {
end = start;
}
possible_cpus = start == 0 ? end + 1 : 0;
break;
}
fclose(fp);
return possible_cpus;
}
struct txq_config {
/* lookup key: __u32 cpu; */
__u16 queue_mapping;
__u16 htb_major;
};
bool map_txq_config_base_setup(int map_fd) {
unsigned int possible_cpus = bpf_num_possible_cpus();
struct txq_config txq_cfg;
__u32 cpu;
int err;
if (map_fd < 0) {
fprintf(stderr, "ERR: (bad map_fd:%d) "
"cannot proceed without access to txq_config map\n",
map_fd);
return false;
}
for (cpu = 0; cpu < possible_cpus; cpu++) {
txq_cfg.queue_mapping = cpu + 1;
txq_cfg.htb_major = cpu + 1;
err = bpf_map_update_elem(map_fd, &cpu, &txq_cfg, 0);
if (err) {
fprintf(stderr,
"ERR: %s() updating cpu-key:%d err(%d):%s\n",
__func__, cpu, errno, strerror(errno));
return false;
}
}
return true;
}

View File

@@ -0,0 +1,11 @@
#include "lqos_kern_skel.h"
#include <stdbool.h>
extern struct lqos_kern * lqos_kern_open();
extern int lqos_kern_load(struct lqos_kern * skel);
extern int tc_attach_egress(int ifindex, bool verbose, struct lqos_kern *obj);
extern int tc_detach_egress(int ifindex, bool verbose, bool flush_hook, const char * ifname);
extern int tc_attach_ingress(int ifindex, bool verbose, struct lqos_kern *obj);
extern int tc_detach_ingress(int ifindex, bool verbose, bool flush_hook, const char * ifname);
extern __u64 max_tracker_ips();
extern bool map_txq_config_base_setup(int map_fd);

View File

@@ -0,0 +1,172 @@
#![allow(dead_code)]
use anyhow::{Error, Result};
use libbpf_sys::{
bpf_map_delete_elem, bpf_map_get_next_key, bpf_map_lookup_elem, bpf_map_update_elem,
bpf_obj_get, BPF_NOEXIST,
};
use std::{
ffi::{c_void, CString},
marker::PhantomData,
ptr::null_mut,
};
/// Represents an underlying BPF map, accessed via the filesystem.
/// `BpfMap` *only* talks to shared (not PER-CPU) variants of maps.
///
/// `K` is the *key* type, indexing the map.
/// `V` is the *value* type, and must exactly match the underlying C data type.
pub(crate) struct BpfMap<K, V> {
fd: i32,
_key_phantom: PhantomData<K>,
_val_phantom: PhantomData<V>,
}
impl<K, V> BpfMap<K, V>
where
K: Default + Clone,
V: Default + Clone,
{
/// Connect to a BPF map via a filename. Connects the internal
/// file descriptor, which is held until the structure is
/// dropped.
pub(crate) fn from_path(filename: &str) -> Result<Self> {
let filename_c = CString::new(filename)?;
let fd = unsafe { bpf_obj_get(filename_c.as_ptr()) };
if fd < 0 {
Err(Error::msg("Unable to open BPF map"))
} else {
Ok(Self {
fd,
_key_phantom: PhantomData,
_val_phantom: PhantomData,
})
}
}
/// Iterates the undlering BPF map, and adds the results
/// to a vector. Each entry contains a `key, value` tuple.
pub(crate) fn dump_vec(&self) -> Vec<(K, V)> {
let mut result = Vec::new();
let mut prev_key: *mut K = null_mut();
let mut key: K = K::default();
let key_ptr: *mut K = &mut key;
let mut value = V::default();
let value_ptr: *mut V = &mut value;
unsafe {
while bpf_map_get_next_key(self.fd, prev_key as *mut c_void, key_ptr as *mut c_void)
== 0
{
bpf_map_lookup_elem(self.fd, key_ptr as *mut c_void, value_ptr as *mut c_void);
result.push((key.clone(), value.clone()));
prev_key = key_ptr;
}
}
result
}
/// Inserts an entry into a BPF map.
/// Use this sparingly, because it briefly pauses XDP access to the
/// underlying map (through internal locking we can't reach from
/// userland).
///
/// ## Arguments
///
/// * `key` - the key to insert.
/// * `value` - the value to insert.
///
/// Returns Ok if insertion succeeded, a generic error (no details yet)
/// if it fails.
pub(crate) fn insert(&mut self, key: &mut K, value: &mut V) -> Result<()> {
let key_ptr: *mut K = key;
let val_ptr: *mut V = value;
let err = unsafe {
bpf_map_update_elem(
self.fd,
key_ptr as *mut c_void,
val_ptr as *mut c_void,
BPF_NOEXIST.into(),
)
};
if err != 0 {
Err(Error::msg(format!("Unable to insert into map ({err})")))
} else {
Ok(())
}
}
/// Deletes an entry from the underlying eBPF map.
/// Use this sparingly, it locks the underlying map in the
/// kernel. This can cause *long* delays under heavy load.
///
/// ## Arguments
///
/// * `key` - the key to delete.
///
/// Return `Ok` if deletion succeeded.
pub(crate) fn delete(&mut self, key: &mut K) -> Result<()> {
let key_ptr: *mut K = key;
let err = unsafe { bpf_map_delete_elem(self.fd, key_ptr as *mut c_void) };
if err != 0 {
Err(Error::msg("Unable to delete from map"))
} else {
Ok(())
}
}
/// Delete all entries in the underlying eBPF map.
/// Use this sparingly, it locks the underlying map. Under
/// heavy load, it WILL eventually terminate - but it might
/// take a very long time. Only use this for cleaning up
/// sparsely allocated map data.
pub(crate) fn clear(&mut self) -> Result<()> {
loop {
let mut key = K::default();
let mut prev_key: *mut K = null_mut();
unsafe {
let key_ptr: *mut K = &mut key;
while bpf_map_get_next_key(self.fd, prev_key as *mut c_void, key_ptr as *mut c_void)
== 0
{
bpf_map_delete_elem(self.fd, key_ptr as *mut c_void);
prev_key = key_ptr;
}
}
key = K::default();
prev_key = null_mut();
unsafe {
let key_ptr: *mut K = &mut key;
if bpf_map_get_next_key(self.fd, prev_key as *mut c_void, key_ptr as *mut c_void)
!= 0
{
break;
}
}
}
Ok(())
}
pub(crate) fn clear_no_repeat(&mut self) -> Result<()> {
let mut key = K::default();
let mut prev_key: *mut K = null_mut();
unsafe {
let key_ptr: *mut K = &mut key;
while bpf_map_get_next_key(self.fd, prev_key as *mut c_void, key_ptr as *mut c_void)
== 0
{
bpf_map_delete_elem(self.fd, key_ptr as *mut c_void);
prev_key = key_ptr;
}
}
Ok(())
}
}
impl<K, V> Drop for BpfMap<K, V> {
fn drop(&mut self) {
let _ = nix::unistd::close(self.fd);
}
}

View File

@@ -0,0 +1,77 @@
use anyhow::{Error, Result};
use libbpf_sys::{
bpf_map_get_next_key, bpf_map_lookup_elem, bpf_obj_get, libbpf_num_possible_cpus,
};
use std::fmt::Debug;
use std::{
ffi::{c_void, CString},
marker::PhantomData,
ptr::null_mut,
};
/// Represents an underlying BPF map, accessed via the filesystem.
/// `BpfMap` *only* talks to PER-CPU variants of maps.
///
/// `K` is the *key* type, indexing the map.
/// `V` is the *value* type, and must exactly match the underlying C data type.
pub(crate) struct BpfPerCpuMap<K, V> {
fd: i32,
_key_phantom: PhantomData<K>,
_val_phantom: PhantomData<V>,
}
impl<K, V> BpfPerCpuMap<K, V>
where
K: Default + Clone,
V: Default + Clone + Debug,
{
/// Connect to a PER-CPU BPF map via a filename. Connects the internal
/// file descriptor, which is held until the structure is
/// dropped. The index of the CPU is *not* specified.
pub(crate) fn from_path(filename: &str) -> Result<Self> {
let filename_c = CString::new(filename)?;
let fd = unsafe { bpf_obj_get(filename_c.as_ptr()) };
if fd < 0 {
Err(Error::msg("Unable to open BPF map"))
} else {
Ok(Self {
fd,
_key_phantom: PhantomData,
_val_phantom: PhantomData,
})
}
}
/// Iterates the entire contents of the underlying eBPF per-cpu map.
/// Each iteration returns one entry per CPU, even if there isn't a
/// CPU-local map entry. Each result is therefore returned as one
/// key and a vector of values.
pub(crate) fn dump_vec(&self) -> Vec<(K, Vec<V>)> {
let mut result = Vec::new();
let num_cpus = unsafe { libbpf_num_possible_cpus() };
let mut prev_key: *mut K = null_mut();
let mut key: K = K::default();
let key_ptr: *mut K = &mut key;
let mut value = vec![V::default(); num_cpus as usize];
let value_ptr = value.as_mut_ptr();
unsafe {
while bpf_map_get_next_key(self.fd, prev_key as *mut c_void, key_ptr as *mut c_void)
== 0
{
bpf_map_lookup_elem(self.fd, key_ptr as *mut c_void, value_ptr as *mut c_void);
result.push((key.clone(), value.clone()));
prev_key = key_ptr;
}
}
result
}
}
impl<K, V> Drop for BpfPerCpuMap<K, V> {
fn drop(&mut self) {
let _ = nix::unistd::close(self.fd);
}
}

View File

@@ -0,0 +1,132 @@
use anyhow::{Error, Result};
use libbpf_sys::{bpf_map_update_elem, bpf_obj_get, libbpf_num_possible_cpus};
use std::{ffi::CString, os::raw::c_void};
//* Provides an interface for querying the number of CPUs eBPF can
//* see, and marking CPUs as available. Currently marks ALL eBPF
//* usable CPUs as available.
pub(crate) struct CpuMapping {
fd_cpu_map: i32,
fd_cpu_available: i32,
fd_txq_config: i32,
}
fn get_map_fd(filename: &str) -> Result<i32> {
let filename_c = CString::new(filename)?;
let fd = unsafe { bpf_obj_get(filename_c.as_ptr()) };
if fd < 0 {
Err(Error::msg("Unable to open BPF map"))
} else {
Ok(fd)
}
}
impl CpuMapping {
pub(crate) fn new() -> Result<Self> {
Ok(Self {
fd_cpu_map: get_map_fd("/sys/fs/bpf/cpu_map")?,
fd_cpu_available: get_map_fd("/sys/fs/bpf/cpus_available")?,
fd_txq_config: get_map_fd("/sys/fs/bpf/map_txq_config")?,
})
}
pub(crate) fn mark_cpus_available(&self) -> Result<()> {
let cpu_count = unsafe { libbpf_num_possible_cpus() } as u32;
let queue_size = 2048u32;
let val_ptr: *const u32 = &queue_size;
for cpu in 0..cpu_count {
println!("Mapping core #{cpu}");
// Insert into the cpu map
let cpu_ptr: *const u32 = &cpu;
let error = unsafe {
bpf_map_update_elem(
self.fd_cpu_map,
cpu_ptr as *const c_void,
val_ptr as *const c_void,
0,
)
};
if error != 0 {
return Err(Error::msg("Unable to map CPU"));
}
// Insert into the available list
let error = unsafe {
bpf_map_update_elem(
self.fd_cpu_available,
cpu_ptr as *const c_void,
cpu_ptr as *const c_void,
0,
)
};
if error != 0 {
return Err(Error::msg("Unable to add to available CPUs list"));
}
} // CPU loop
Ok(())
}
pub(crate) fn setup_base_txq_config(&self) -> Result<()> {
use crate::lqos_kernel::bpf::map_txq_config_base_setup;
// Should we shell out to the C and do it the easy way?
let result = unsafe { map_txq_config_base_setup(self.fd_txq_config) };
if !result {
Err(Error::msg("Unable to setup TXQ map"))
} else {
Ok(())
}
}
}
impl Drop for CpuMapping {
fn drop(&mut self) {
let _ = nix::unistd::close(self.fd_cpu_available);
let _ = nix::unistd::close(self.fd_cpu_map);
let _ = nix::unistd::close(self.fd_txq_config);
}
}
/// Emulates xd_setup from cpumap
pub(crate) fn xps_setup_default_disable(interface: &str) -> Result<()> {
use std::io::Write;
println!("xps_setup");
let queues = sorted_txq_xps_cpus(interface)?;
for (cpu, xps_cpu) in queues.iter().enumerate() {
let mask = cpu_to_mask_disabled(cpu);
let mut f = std::fs::OpenOptions::new().write(true).open(xps_cpu)?;
f.write_all(&mask.to_string().as_bytes())?;
f.flush()?;
println!("Mapped TX queue for CPU {cpu}");
}
Ok(())
}
fn sorted_txq_xps_cpus(interface: &str) -> Result<Vec<String>> {
let mut result = Vec::new();
let paths = std::fs::read_dir(&format!("/sys/class/net/{interface}/queues/"))?;
for path in paths {
if let Ok(path) = &path {
if path.path().is_dir() {
if let Some(filename) = path.path().file_name() {
let base_fn = format!(
"/sys/class/net/{interface}/queues/{}/xps_cpus",
filename.to_str().unwrap()
);
if std::path::Path::new(&base_fn).exists() {
result.push(base_fn);
}
}
}
}
}
result.sort();
Ok(result)
}
fn cpu_to_mask_disabled(_cpu: usize) -> usize {
0
}

View File

@@ -0,0 +1,15 @@
#[repr(C)]
#[derive(Clone)]
pub struct IpHashData {
pub cpu: u32,
pub tc_handle: u32,
}
impl Default for IpHashData {
fn default() -> Self {
Self {
cpu: 0,
tc_handle: 0,
}
}
}

View File

@@ -0,0 +1,15 @@
#[repr(C)]
#[derive(Clone)]
pub struct IpHashKey {
pub prefixlen: u32,
pub address: [u8; 16],
}
impl Default for IpHashKey {
fn default() -> Self {
Self {
prefixlen: 0,
address: [0xFF; 16],
}
}
}

View File

@@ -0,0 +1,127 @@
use anyhow::{Error, Result};
use lqos_bus::TcHandle;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
pub(crate) struct IpToMap {
pub(crate) subnet: IpAddr,
pub(crate) prefix: u32,
pub(crate) tc_handle: TcHandle,
pub(crate) cpu: u32,
}
impl IpToMap {
pub(crate) fn new(address: &str, tc_handle: TcHandle, cpu: u32) -> Result<Self> {
let address_part; // Filled in later
let mut subnet_part = 128;
if address.contains("/") {
let parts: Vec<&str> = address.split('/').collect();
address_part = parts[0].to_string();
subnet_part = parts[1].replace("/", "").parse()?;
} else {
address_part = address.to_string();
}
let subnet = if address_part.contains(":") {
// It's an IPv6
let ipv6 = address_part.parse::<Ipv6Addr>()?;
IpAddr::V6(ipv6)
} else {
// It's an IPv4
if subnet_part != 128 {
subnet_part += 96;
}
let ipv4 = address_part.parse::<Ipv4Addr>()?;
IpAddr::V4(ipv4)
};
if subnet_part > 128 {
return Err(Error::msg("Invalid subnet mask"));
}
Ok(Self {
subnet,
prefix: subnet_part,
tc_handle,
cpu,
})
}
pub(crate) fn handle(&self) -> u32 {
self.tc_handle.as_u32()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_ipv4_single() {
let map = IpToMap::new("1.2.3.4", TcHandle::from_string("1:2").unwrap(), 1).unwrap();
let rust_ip: IpAddr = "1.2.3.4".parse().unwrap();
assert_eq!(rust_ip, map.subnet);
assert_eq!(map.prefix, 128);
assert_eq!(map.tc_handle.to_string(), "1:2");
assert_eq!(map.cpu, 1);
}
#[test]
fn parse_ipv4_subnet() {
let map = IpToMap::new("1.2.3.0/24", TcHandle::from_string("1:2").unwrap(), 1).unwrap();
let rust_ip: IpAddr = "1.2.3.0".parse().unwrap();
assert_eq!(rust_ip, map.subnet);
assert_eq!(map.prefix, 24 + 96);
assert_eq!(map.tc_handle.to_string(), "1:2");
assert_eq!(map.cpu, 1);
}
#[test]
fn parse_ipv4_invalid_ip() {
let map = IpToMap::new("1.2.3.256/24", TcHandle::from_string("1:2").unwrap(), 1);
assert!(map.is_err());
}
#[test]
fn parse_ipv4_super_invalid_ip() {
let map = IpToMap::new("I like sheep", TcHandle::from_string("1:2").unwrap(), 1);
assert!(map.is_err());
}
#[test]
fn parse_ipv4_invalid_cidr() {
let map = IpToMap::new("1.2.3.256/33", TcHandle::from_string("1:2").unwrap(), 1);
assert!(map.is_err());
}
#[test]
fn parse_ipv4_negative_cidr() {
let map = IpToMap::new("1.2.3.256/-1", TcHandle::from_string("1:2").unwrap(), 1);
assert!(map.is_err());
}
#[test]
fn parse_ipv6_single() {
let map = IpToMap::new("dead::beef", TcHandle::from_string("1:2").unwrap(), 1).unwrap();
let rust_ip: IpAddr = "dead::beef".parse().unwrap();
assert_eq!(rust_ip, map.subnet);
assert_eq!(map.prefix, 128);
assert_eq!(map.tc_handle.to_string(), "1:2");
assert_eq!(map.cpu, 1);
}
#[test]
fn parse_ipv6_subnet() {
let map = IpToMap::new("dead:beef::/64", TcHandle::from_string("1:2").unwrap(), 1).unwrap();
let rust_ip: IpAddr = "dead:beef::".parse().unwrap();
assert_eq!(rust_ip, map.subnet);
assert_eq!(map.prefix, 64);
assert_eq!(map.tc_handle.to_string(), "1:2");
assert_eq!(map.cpu, 1);
}
#[test]
fn parse_ipv6_invalid_ip() {
let map = IpToMap::new("dead:beef", TcHandle::from_string("1:2").unwrap(), 1);
assert!(map.is_err());
}
}

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