mirror of
https://github.com/LibreQoE/LibreQoS.git
synced 2025-02-25 18:55:32 -06:00
Merge branch 'v1.4-pre-alpha-rust-integration'
This commit is contained in:
20
.github/workflows/rust.yml
vendored
Normal file
20
.github/workflows/rust.yml
vendored
Normal 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
18
.gitignore
vendored
@@ -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
4
.gitmodules
vendored
@@ -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
|
||||
|
||||
|
||||
105
src/LibreQoS.py
105
src/LibreQoS.py
@@ -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
117
src/TESTING-1.4.md
Normal 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`
|
||||
10
src/bin/lqos_node_manager.service.example
Normal file
10
src/bin/lqos_node_manager.service.example
Normal 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
|
||||
10
src/bin/lqosd.service.example
Normal file
10
src/bin/lqosd.service.example
Normal 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
40
src/build_rust.sh
Executable 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
@@ -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 = {}
|
||||
|
||||
@@ -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
34
src/lqos.example
Normal 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
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
24
src/rust/Cargo.toml
Normal 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
32
src/rust/README.md
Normal 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.
|
||||
17
src/rust/lqos_bus/Cargo.toml
Normal file
17
src/rust/lqos_bus/Cargo.toml
Normal 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"
|
||||
5
src/rust/lqos_bus/build.rs
Normal file
5
src/rust/lqos_bus/build.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
cc::Build::new()
|
||||
.file("src/tc_handle_parser.c")
|
||||
.compile("tc_handle_parse.o");
|
||||
}
|
||||
76
src/rust/lqos_bus/src/bus/mod.rs
Normal file
76
src/rust/lqos_bus/src/bus/mod.rs
Normal 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]);
|
||||
}
|
||||
}
|
||||
20
src/rust/lqos_bus/src/bus/reply.rs
Normal file
20
src/rust/lqos_bus/src/bus/reply.rs
Normal 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>,
|
||||
}
|
||||
94
src/rust/lqos_bus/src/bus/request.rs
Normal file
94
src/rust/lqos_bus/src/bus/request.rs
Normal 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,
|
||||
}
|
||||
61
src/rust/lqos_bus/src/bus/response.rs
Normal file
61
src/rust/lqos_bus/src/bus/response.rs
Normal 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),
|
||||
}
|
||||
16
src/rust/lqos_bus/src/bus/session.rs
Normal file
16
src/rust/lqos_bus/src/bus/session.rs
Normal 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>,
|
||||
}
|
||||
65
src/rust/lqos_bus/src/ip_stats.rs
Normal file
65
src/rust/lqos_bus/src/ip_stats.rs
Normal 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,
|
||||
}
|
||||
21
src/rust/lqos_bus/src/lib.rs
Normal file
21
src/rust/lqos_bus/src/lib.rs
Normal 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};
|
||||
|
||||
133
src/rust/lqos_bus/src/tc_handle.rs
Normal file
133
src/rust/lqos_bus/src/tc_handle.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/rust/lqos_bus/src/tc_handle_parser.c
Normal file
46
src/rust/lqos_bus/src/tc_handle_parser.c
Normal 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;
|
||||
}
|
||||
14
src/rust/lqos_config/Cargo.toml
Normal file
14
src/rust/lqos_config/Cargo.toml
Normal 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" ] }
|
||||
15
src/rust/lqos_config/README.md
Normal file
15
src/rust/lqos_config/README.md
Normal 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`)
|
||||
220
src/rust/lqos_config/src/authentication.rs
Normal file
220
src/rust/lqos_config/src/authentication.rs
Normal 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
|
||||
}
|
||||
}
|
||||
118
src/rust/lqos_config/src/etc.rs
Normal file
118
src/rust/lqos_config/src/etc.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
19
src/rust/lqos_config/src/lib.rs
Normal file
19
src/rust/lqos_config/src/lib.rs
Normal 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};
|
||||
287
src/rust/lqos_config/src/libre_qos_config.rs
Normal file
287
src/rust/lqos_config/src/libre_qos_config.rs
Normal 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("'", "")
|
||||
}
|
||||
40
src/rust/lqos_config/src/program_control.rs
Normal file
40
src/rust/lqos_config/src/program_control.rs
Normal 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)
|
||||
}
|
||||
216
src/rust/lqos_config/src/shaped_devices/mod.rs
Normal file
216
src/rust/lqos_config/src/shaped_devices/mod.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
87
src/rust/lqos_config/src/shaped_devices/serializable.rs
Normal file
87
src/rust/lqos_config/src/shaped_devices/serializable.rs
Normal 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()
|
||||
}
|
||||
170
src/rust/lqos_config/src/shaped_devices/shaped_device.rs
Normal file
170
src/rust/lqos_config/src/shaped_devices/shaped_device.rs
Normal 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
|
||||
}
|
||||
}
|
||||
20
src/rust/lqos_node_manager/Cargo.toml
Normal file
20
src/rust/lqos_node_manager/Cargo.toml
Normal 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"
|
||||
3
src/rust/lqos_node_manager/Rocket.toml
Normal file
3
src/rust/lqos_node_manager/Rocket.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[default]
|
||||
port = 9123
|
||||
address = "::"
|
||||
130
src/rust/lqos_node_manager/src/auth_guard.rs
Normal file
130
src/rust/lqos_node_manager/src/auth_guard.rs
Normal 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())
|
||||
}
|
||||
50
src/rust/lqos_node_manager/src/cache_control.rs
Normal file
50
src/rust/lqos_node_manager/src/cache_control.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/rust/lqos_node_manager/src/config_control.rs
Normal file
70
src/rust/lqos_node_manager/src/config_control.rs
Normal 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) = ð.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())
|
||||
}
|
||||
90
src/rust/lqos_node_manager/src/main.rs
Normal file
90
src/rust/lqos_node_manager/src/main.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
138
src/rust/lqos_node_manager/src/queue_info.rs
Normal file
138
src/rust/lqos_node_manager/src/queue_info.rs
Normal 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!"))
|
||||
}
|
||||
92
src/rust/lqos_node_manager/src/shaped_devices.rs
Normal file
92
src/rust/lqos_node_manager/src/shaped_devices.rs
Normal 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))
|
||||
}
|
||||
136
src/rust/lqos_node_manager/src/static_pages.rs
Normal file
136
src/rust/lqos_node_manager/src/static_pages.rs
Normal 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())
|
||||
}
|
||||
12
src/rust/lqos_node_manager/src/tracker/cache/cpu_ram.rs
vendored
Normal file
12
src/rust/lqos_node_manager/src/tracker/cache/cpu_ram.rs
vendored
Normal 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]);
|
||||
}
|
||||
19
src/rust/lqos_node_manager/src/tracker/cache/lqosd_stats.rs
vendored
Normal file
19
src/rust/lqos_node_manager/src/tracker/cache/lqosd_stats.rs
vendored
Normal 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));
|
||||
}
|
||||
13
src/rust/lqos_node_manager/src/tracker/cache/mod.rs
vendored
Normal file
13
src/rust/lqos_node_manager/src/tracker/cache/mod.rs
vendored
Normal 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::*;
|
||||
18
src/rust/lqos_node_manager/src/tracker/cache/shaped_devices.rs
vendored
Normal file
18
src/rust/lqos_node_manager/src/tracker/cache/shaped_devices.rs
vendored
Normal 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());
|
||||
}
|
||||
73
src/rust/lqos_node_manager/src/tracker/cache/throughput.rs
vendored
Normal file
73
src/rust/lqos_node_manager/src/tracker/cache/throughput.rs
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
151
src/rust/lqos_node_manager/src/tracker/cache_manager.rs
Normal file
151
src/rust/lqos_node_manager/src/tracker/cache_manager.rs
Normal 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(())
|
||||
}
|
||||
135
src/rust/lqos_node_manager/src/tracker/mod.rs
Normal file
135
src/rust/lqos_node_manager/src/tracker/mod.rs
Normal 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)
|
||||
}
|
||||
24
src/rust/lqos_node_manager/src/unknown_devices.rs
Normal file
24
src/rust/lqos_node_manager/src/unknown_devices.rs
Normal 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))
|
||||
}
|
||||
446
src/rust/lqos_node_manager/static/circuit_queue.html
Normal file
446
src/rust/lqos_node_manager/static/circuit_queue.html
Normal 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" /> 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>
|
||||
642
src/rust/lqos_node_manager/static/config.html
Normal file
642
src/rust/lqos_node_manager/static/config.html
Normal 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" /> 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>
|
||||
BIN
src/rust/lqos_node_manager/static/favicon.ico
Normal file
BIN
src/rust/lqos_node_manager/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 B |
93
src/rust/lqos_node_manager/static/first_run.html
Normal file
93
src/rust/lqos_node_manager/static/first_run.html
Normal 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>
|
||||
73
src/rust/lqos_node_manager/static/login.html
Normal file
73
src/rust/lqos_node_manager/static/login.html
Normal 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>
|
||||
18
src/rust/lqos_node_manager/static/lqos.css
Normal file
18
src/rust/lqos_node_manager/static/lqos.css
Normal 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; }
|
||||
188
src/rust/lqos_node_manager/static/lqos.js
Normal file
188
src/rust/lqos_node_manager/static/lqos.js
Normal 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 ba’lu’’a’", // Is this seat taken?
|
||||
"vjIjatlh", // speak
|
||||
"pe’vIl mu’qaDmey", // curse well
|
||||
"nuqDaq ‘oH puchpa’’e’", // where's the bathroom?
|
||||
"nuqDaq ‘oH tach’e’", // Where's the bar?
|
||||
"tera’ngan Soj lujab’a’", // 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
|
||||
"Heghlu’meH 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>`;
|
||||
340
src/rust/lqos_node_manager/static/main.html
Normal file
340
src/rust/lqos_node_manager/static/main.html
Normal 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" /> 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>
|
||||
170
src/rust/lqos_node_manager/static/shaped-add.html
Normal file
170
src/rust/lqos_node_manager/static/shaped-add.html
Normal 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" /> 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>
|
||||
166
src/rust/lqos_node_manager/static/shaped.html
Normal file
166
src/rust/lqos_node_manager/static/shaped.html
Normal 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" /> 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>
|
||||
13
src/rust/lqos_node_manager/static/tinylogo.svg
Normal file
13
src/rust/lqos_node_manager/static/tinylogo.svg
Normal 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 |
126
src/rust/lqos_node_manager/static/unknown-ip.html
Normal file
126
src/rust/lqos_node_manager/static/unknown-ip.html
Normal 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" /> 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>
|
||||
7
src/rust/lqos_node_manager/static/vendor/bootstrap.bundle.min.js
vendored
Normal file
7
src/rust/lqos_node_manager/static/vendor/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
src/rust/lqos_node_manager/static/vendor/bootstrap.min.css
vendored
Normal file
7
src/rust/lqos_node_manager/static/vendor/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/rust/lqos_node_manager/static/vendor/fa-webfont.ttf
vendored
Normal file
BIN
src/rust/lqos_node_manager/static/vendor/fa-webfont.ttf
vendored
Normal file
Binary file not shown.
2
src/rust/lqos_node_manager/static/vendor/jquery.min.js
vendored
Normal file
2
src/rust/lqos_node_manager/static/vendor/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/rust/lqos_node_manager/static/vendor/klingon.ttf
vendored
Normal file
BIN
src/rust/lqos_node_manager/static/vendor/klingon.ttf
vendored
Normal file
Binary file not shown.
65
src/rust/lqos_node_manager/static/vendor/plotly-2.16.1.min.js
vendored
Normal file
65
src/rust/lqos_node_manager/static/vendor/plotly-2.16.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
src/rust/lqos_node_manager/static/vendor/solid.min.css
vendored
Normal file
4
src/rust/lqos_node_manager/static/vendor/solid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
14
src/rust/lqos_python/Cargo.toml
Normal file
14
src/rust/lqos_python/Cargo.toml
Normal 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"
|
||||
30
src/rust/lqos_python/src/blocking.rs
Normal file
30
src/rust/lqos_python/src/blocking.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
122
src/rust/lqos_python/src/lib.rs
Normal file
122
src/rust/lqos_python/src/lib.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
15
src/rust/lqos_sys/Cargo.toml
Normal file
15
src/rust/lqos_sys/Cargo.toml
Normal 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"
|
||||
7
src/rust/lqos_sys/README.md
Normal file
7
src/rust/lqos_sys/README.md
Normal 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
146
src/rust/lqos_sys/build.rs
Normal 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!");
|
||||
}
|
||||
70
src/rust/lqos_sys/src/bifrost_maps.rs
Normal file
70
src/rust/lqos_sys/src/bifrost_maps.rs
Normal 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(())
|
||||
}
|
||||
45
src/rust/lqos_sys/src/bpf/common/bifrost.h
Normal file
45
src/rust/lqos_sys/src/bpf/common/bifrost.h
Normal 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");
|
||||
43
src/rust/lqos_sys/src/bpf/common/cpu_map.h
Normal file
43
src/rust/lqos_sys/src/bpf/common/cpu_map.h
Normal 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");
|
||||
13
src/rust/lqos_sys/src/bpf/common/debug.h
Normal file
13
src/rust/lqos_sys/src/bpf/common/debug.h
Normal 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__); \
|
||||
})
|
||||
290
src/rust/lqos_sys/src/bpf/common/dissector.h
Normal file
290
src/rust/lqos_sys/src/bpf/common/dissector.h
Normal 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;
|
||||
}
|
||||
}
|
||||
223
src/rust/lqos_sys/src/bpf/common/dissector_tc.h
Normal file
223
src/rust/lqos_sys/src/bpf/common/dissector_tc.h
Normal 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;
|
||||
}
|
||||
}
|
||||
43
src/rust/lqos_sys/src/bpf/common/ip_hash.h
Normal file
43
src/rust/lqos_sys/src/bpf/common/ip_hash.h
Normal 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
|
||||
);
|
||||
}
|
||||
170
src/rust/lqos_sys/src/bpf/common/lpm.h
Normal file
170
src/rust/lqos_sys/src/bpf/common/lpm.h
Normal 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;
|
||||
}
|
||||
16
src/rust/lqos_sys/src/bpf/common/maximums.h
Normal file
16
src/rust/lqos_sys/src/bpf/common/maximums.h
Normal 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
|
||||
4
src/rust/lqos_sys/src/bpf/common/skb_safety.h
Normal file
4
src/rust/lqos_sys/src/bpf/common/skb_safety.h
Normal 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)
|
||||
@@ -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 */
|
||||
777
src/rust/lqos_sys/src/bpf/common/tcp_rtt.h
Normal file
777
src/rust/lqos_sys/src/bpf/common/tcp_rtt.h
Normal 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 */
|
||||
70
src/rust/lqos_sys/src/bpf/common/throughput.h
Normal file
70
src/rust/lqos_sys/src/bpf/common/throughput.h
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
309
src/rust/lqos_sys/src/bpf/lqos_kern.c
Normal file
309
src/rust/lqos_sys/src/bpf/lqos_kern.c
Normal 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";
|
||||
306
src/rust/lqos_sys/src/bpf/wrapper.c
Normal file
306
src/rust/lqos_sys/src/bpf/wrapper.c
Normal 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;
|
||||
}
|
||||
11
src/rust/lqos_sys/src/bpf/wrapper.h
Normal file
11
src/rust/lqos_sys/src/bpf/wrapper.h
Normal 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);
|
||||
172
src/rust/lqos_sys/src/bpf_map.rs
Normal file
172
src/rust/lqos_sys/src/bpf_map.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
77
src/rust/lqos_sys/src/bpf_per_cpu_map.rs
Normal file
77
src/rust/lqos_sys/src/bpf_per_cpu_map.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
132
src/rust/lqos_sys/src/cpu_map.rs
Normal file
132
src/rust/lqos_sys/src/cpu_map.rs
Normal 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
|
||||
}
|
||||
15
src/rust/lqos_sys/src/ip_mapping/ip_hash_data.rs
Normal file
15
src/rust/lqos_sys/src/ip_mapping/ip_hash_data.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/rust/lqos_sys/src/ip_mapping/ip_hash_key.rs
Normal file
15
src/rust/lqos_sys/src/ip_mapping/ip_hash_key.rs
Normal 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],
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/rust/lqos_sys/src/ip_mapping/ip_to_map.rs
Normal file
127
src/rust/lqos_sys/src/ip_mapping/ip_to_map.rs
Normal 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
Reference in New Issue
Block a user