shaper: move pipe & queue configuration to dnctl service (#8404)

if no shaper (ipfw) rules are present, or these rules are disabled, ipfw will be disabled as well (firewall_enable="NO" and rc.ipfw onestop).

Traffic shaped via pf will not show up in the stats output of dnctl pipe|queue|sched show. Also, there is currently no logic to associate pipes/queues with pf rules.
This commit is contained in:
Stephan de Wit 2025-03-06 10:32:13 +01:00 committed by GitHub
parent 3a1b88bf90
commit 3bf818348c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 137 additions and 86 deletions

5
plist
View File

@ -1273,6 +1273,7 @@
/usr/local/opnsense/scripts/routes/show_routes.py
/usr/local/opnsense/scripts/shaper/dummynet_stats.py
/usr/local/opnsense/scripts/shaper/lib/__init__.py
/usr/local/opnsense/scripts/shaper/setup.sh
/usr/local/opnsense/scripts/shaper/update_tables
/usr/local/opnsense/scripts/shell/banner.php
/usr/local/opnsense/scripts/shell/defaults.php
@ -1364,6 +1365,7 @@
/usr/local/opnsense/service/conf/actions.d/actions_netflow.conf
/usr/local/opnsense/service/conf/actions.d/actions_openssh.conf
/usr/local/opnsense/service/conf/actions.d/actions_openvpn.conf
/usr/local/opnsense/service/conf/actions.d/actions_shaper.conf
/usr/local/opnsense/service/conf/actions.d/actions_syslog.conf
/usr/local/opnsense/service/conf/actions.d/actions_system.conf
/usr/local/opnsense/service/conf/actions.d/actions_template.conf
@ -1452,6 +1454,9 @@
/usr/local/opnsense/service/templates/OPNsense/Sample/sub1/example_sub1.txt
/usr/local/opnsense/service/templates/OPNsense/Sample/sub2/+TARGETS
/usr/local/opnsense/service/templates/OPNsense/Sample/sub2/example_sub2.txt
/usr/local/opnsense/service/templates/OPNsense/Shaper/+TARGETS
/usr/local/opnsense/service/templates/OPNsense/Shaper/dnctl.conf
/usr/local/opnsense/service/templates/OPNsense/Shaper/rc.conf.d
/usr/local/opnsense/service/templates/OPNsense/Syslog/+TARGETS
/usr/local/opnsense/service/templates/OPNsense/Syslog/local/README
/usr/local/opnsense/service/templates/OPNsense/Syslog/local/audit.conf

View File

@ -27,10 +27,6 @@
# script to glue standard ipfw rc scripting to OPNsense ruleset
# see auto generated file /etc/rc.conf.d/ipfw for details
# sysctl settings
sysctl net.inet.ip.dummynet.io_fast=1 > /dev/null
sysctl net.inet.ip.dummynet.hash_size=256 > /dev/null
# reload ipfw rules
ipfw -q /usr/local/etc/ipfw.rules
# load tables

View File

@ -41,24 +41,29 @@ use OPNsense\TrafficShaper\TrafficShaper;
class ServiceController extends ApiControllerBase
{
/**
* reconfigure ipfw, generate config and reload
* reconfigure shaper/ipfw, generate config and reload
*/
public function reconfigureAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$backend->configdRun('template reload OPNsense/Shaper');
$backend->configdRun('template reload OPNsense/IPFW');
$bckresult = trim($backend->configdRun("ipfw reload"));
if ($bckresult == "OK") {
$status = "ok";
} else {
$status = "error reloading shaper (" . $bckresult . ")";
$result = trim($backend->configdRun("shaper reload"));
if ($result != "OK") {
return ["status" => "error reloading shaper (" . $result . ")"];
}
return array("status" => $status);
} else {
return array("status" => "failed");
$result = trim($backend->configdRun("ipfw reload"));
if ($result != "OK") {
return ["status" => "error reloading ipfw (" . $result . ")"];
}
return ["status" => "ok"];
}
return ["status" => "failed"];
}
/**
@ -69,10 +74,11 @@ class ServiceController extends ApiControllerBase
if ($this->request->isPost()) {
$backend = new Backend();
$status = trim($backend->configdRun("ipfw flush"));
$status = trim($backend->configdRun("shaper reload"));
$status = trim($backend->configdRun("ipfw reload"));
return array("status" => $status);
return ["status" => $status];
} else {
return array("status" => "failed");
return ["status" => "failed"];
}
}
@ -81,15 +87,14 @@ class ServiceController extends ApiControllerBase
*/
public function statisticsAction()
{
$result = array("status" => "failed");
$result = ["status" => "failed"];
if ($this->request->isGet()) {
$ipfwstats = json_decode((new Backend())->configdRun("ipfw stats"), true);
$ipfwstats = json_decode((new Backend())->configdRun("shaper stats"), true);
if ($ipfwstats != null) {
// ipfw stats are structured as they would be using the various ipfw commands, let's reformat
// into something easier to handle from the UI and attach model data.
// reformat into something easier to handle from the UI and attach model data.
$result['status'] = "ok";
$result['items'] = array();
$pipenrs = array();
$result['items'] = [];
$pipenrs = [];
if (!empty($ipfwstats['pipes'])) {
$shaperModel = new TrafficShaper();

View File

@ -81,7 +81,7 @@ def trim_dict(payload):
def parse_ipfw_pipes():
result = dict()
pipetxt = subprocess.run(['/sbin/ipfw', 'pipe', 'show'], capture_output=True, text=True).stdout.strip()
pipetxt = subprocess.run(['/sbin/dnctl', 'pipe', 'show'], capture_output=True, text=True).stdout.strip()
current_pipe = None
current_pipe_header = False
for line in ("%s\n000000X" % pipetxt).split('\n'):
@ -125,7 +125,7 @@ def parse_ipfw_pipes():
def parse_ipfw_queues():
result = dict()
queuetxt = subprocess.run(['/sbin/ipfw', 'queue', 'show'], capture_output=True, text=True).stdout.strip()
queuetxt = subprocess.run(['/sbin/dnctl', 'queue', 'show'], capture_output=True, text=True).stdout.strip()
current_queue = None
current_queue_header = False
for line in ("%s\nq000000X" % queuetxt).split('\n'):
@ -149,7 +149,7 @@ def parse_ipfw_queues():
def parse_ipfw_scheds():
result = dict()
schedtxt = subprocess.run(['/sbin/ipfw', 'sched', 'show'], capture_output=True, text=True).stdout.strip()
schedtxt = subprocess.run(['/sbin/dnctl', 'sched', 'show'], capture_output=True, text=True).stdout.strip()
current_sched = None
for line in ("%s\n000000X" % schedtxt).split('\n'):
if len(line) == 0:

View File

@ -0,0 +1,29 @@
#!/bin/sh
# Copyright (c) 2025 Deciso B.V.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# sysctl settings
sysctl net.inet.ip.dummynet.io_fast=1 > /dev/null
sysctl net.inet.ip.dummynet.hash_size=256 > /dev/null

View File

@ -1,5 +1,5 @@
[reload]
command:/etc/rc.d/ipfw onestart; /usr/local/etc/rc.ipfw.postload
command:/etc/rc.d/ipfw enabled && /etc/rc.d/ipfw start || /etc/rc.d/ipfw onestop; /usr/local/etc/rc.ipfw.postload
parameters:
type:script
message:restarting ipfw
@ -9,9 +9,3 @@ command:/usr/local/etc/rc.ipfw.flush_all
parameters:
type:script
message:flush all ipfw rules
[stats]
command:/usr/local/opnsense/scripts/shaper/dummynet_stats.py
parameters:
type:script_output
message:request dummynet stats

View File

@ -0,0 +1,11 @@
[reload]
command:/etc/rc.d/dnctl start
parameters:
type:script
message:restarting dummynet
[stats]
command:/usr/local/opnsense/scripts/shaper/dummynet_stats.py
parameters:
type:script_output
message:request dummynet stats

View File

@ -6,53 +6,6 @@
#======================================================================================
flush
#======================================================================================
# define dummynet pipes
#======================================================================================
{% if helpers.exists('OPNsense.TrafficShaper.pipes.pipe') %}
{% for pipe in helpers.toList('OPNsense.TrafficShaper.pipes.pipe') %}
pipe {{ pipe.number }} config bw {{ pipe.bandwidth }}{{ pipe.bandwidthMetric }}/s{%
if pipe.queue %} queue {{ pipe.queue }}{%
if pipe.queueMetric != 'slots' %}{{pipe.queueMetric}}{% endif %}{% endif
%}{% if pipe.buckets %} buckets {{ pipe.buckets }}{% endif
%}{% if pipe.mask != 'none' %} mask {{ pipe.mask }} 0xffffffff {% endif %}{%
if pipe.delay|default('') != '' %} delay {{pipe.delay}} {% endif %} type {%
if pipe.scheduler|default('') != '' %} {{pipe.scheduler}} {% else %} wf2q+ {% endif %}{%
if pipe.codel_enable|default('0') == '1' and pipe.scheduler != 'fq_codel' %} codel {% endif %}{%
if pipe.codel_enable|default('0') == '1' or pipe.scheduler == 'fq_codel' %}{%
if pipe.codel_target|default('') != ''%} target {{pipe.codel_target}} {% endif %}{%
if pipe.codel_interval|default('') != ''%} interval {{pipe.codel_interval}} {% endif %}{%
if pipe.codel_ecn_enable|default('0') == '1'%} ecn {% else %} noecn {% endif %} {%
if pipe.scheduler == 'fq_codel' %} {%
if pipe.fqcodel_quantum|default('') != '' %} quantum {{pipe.fqcodel_quantum}} {% endif %} {%
if pipe.fqcodel_limit|default('') != '' %} limit {{pipe.fqcodel_limit}} {% endif %} {%
if pipe.fqcodel_flows|default('') != '' %} flows {{pipe.fqcodel_flows}} {% endif %}
{% endif %}{%
elif pipe.pie_enable|default('0') == '1' and pipe.scheduler != 'fq_pie' %} pie {% endif %}
{% endfor %}
{% endif %}
#======================================================================================
# define dummynet queues
#======================================================================================
{% if helpers.exists('OPNsense.TrafficShaper.queues.queue') %}
{% for queue in helpers.toList('OPNsense.TrafficShaper.queues.queue') %}
{% if helpers.getUUIDtag(queue.pipe) in ['pipe'] %}
queue {{ queue.number }} config pipe {{ helpers.getUUID(queue.pipe).number
}}{% if queue.buckets %} buckets {{ queue.buckets }}{% endif %}{% if queue.mask != 'none' %} mask {{ queue.mask }} 0xffffffff {% endif %} weight {{ queue.weight }}{%
if queue.codel_enable|default('0') == '1' %} codel {%
if queue.codel_target|default('') != ''%} target {{queue.codel_target}} {% endif %}{%
if queue.codel_interval|default('') != ''%} interval {{queue.codel_interval}} {% endif %}{%
if queue.codel_ecn_enable|default('0') == '1'%} ecn {% else %} noecn {% endif %}{%
elif queue.pie_enable|default('0') == '1' %} pie
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
#======================================================================================
# general purpose rules 1...1000
#======================================================================================

View File

@ -7,17 +7,16 @@
{% endfor %}
{% endif %}
{# collect enabled #}
{% set shapers = [] %}
{% set rules = [] %}
{% if helpers.exists('OPNsense.TrafficShaper') %}
{% if helpers.exists('OPNsense.TrafficShaper.pipes.pipe') %}
{% for pipe in helpers.toList('OPNsense.TrafficShaper.pipes.pipe') %}
{% if pipe.enabled|default('0') == '1' %}
{% do shapers.append(cp_key) %}
{% endif%}
{% if helpers.exists('OPNsense.TrafficShaper.rules.rule') %}
{% for rule in helpers.toList('OPNsense.TrafficShaper.rules.rule') %}
{% if rule.enabled|default("0") == '1' %}
{% do rules.append(rule) %}
{% endif %}
{% endfor%}
{% endif %}
{% endif %}
dummynet_enable="YES"
firewall_enable="{% if shapers or cp_zones %}YES{% else %}NO{% endif %}"
firewall_enable="{% if cp_zones or rules %}YES{% else %}NO{% endif %}"
firewall_script="/usr/local/etc/rc.ipfw"
ipfw_defer="YES"

View File

@ -0,0 +1,2 @@
rc.conf.d:/etc/rc.conf.d/dnctl
dnctl.conf:/usr/local/etc/dnctl.conf

View File

@ -0,0 +1,45 @@
#======================================================================================
# define dummynet pipes
#======================================================================================
{% if helpers.exists('OPNsense.TrafficShaper.pipes.pipe') %}
{% for pipe in helpers.toList('OPNsense.TrafficShaper.pipes.pipe') %}
pipe {{ pipe.number }} config bw {{ pipe.bandwidth }}{{ pipe.bandwidthMetric }}/s{%
if pipe.queue %} queue {{ pipe.queue }}{%
if pipe.queueMetric != 'slots' %}{{pipe.queueMetric}}{% endif %}{% endif
%}{% if pipe.buckets %} buckets {{ pipe.buckets }}{% endif
%}{% if pipe.mask != 'none' %} mask {{ pipe.mask }} 0xffffffff {% endif %}{%
if pipe.delay|default('') != '' %} delay {{pipe.delay}} {% endif %} type {%
if pipe.scheduler|default('') != '' %} {{pipe.scheduler}} {% else %} wf2q+ {% endif %}{%
if pipe.codel_enable|default('0') == '1' and pipe.scheduler != 'fq_codel' %} codel {% endif %}{%
if pipe.codel_enable|default('0') == '1' or pipe.scheduler == 'fq_codel' %}{%
if pipe.codel_target|default('') != ''%} target {{pipe.codel_target}} {% endif %}{%
if pipe.codel_interval|default('') != ''%} interval {{pipe.codel_interval}} {% endif %}{%
if pipe.codel_ecn_enable|default('0') == '1'%} ecn {% else %} noecn {% endif %} {%
if pipe.scheduler == 'fq_codel' %} {%
if pipe.fqcodel_quantum|default('') != '' %} quantum {{pipe.fqcodel_quantum}} {% endif %} {%
if pipe.fqcodel_limit|default('') != '' %} limit {{pipe.fqcodel_limit}} {% endif %} {%
if pipe.fqcodel_flows|default('') != '' %} flows {{pipe.fqcodel_flows}} {% endif %}
{% endif %}{%
elif pipe.pie_enable|default('0') == '1' and pipe.scheduler != 'fq_pie' %} pie {% endif %}
{% endfor %}
{% endif %}
#======================================================================================
# define dummynet queues
#======================================================================================
{% if helpers.exists('OPNsense.TrafficShaper.queues.queue') %}
{% for queue in helpers.toList('OPNsense.TrafficShaper.queues.queue') %}
{% if helpers.getUUIDtag(queue.pipe) in ['pipe'] %}
queue {{ queue.number }} config pipe {{ helpers.getUUID(queue.pipe).number
}}{% if queue.buckets %} buckets {{ queue.buckets }}{% endif %}{% if queue.mask != 'none' %} mask {{ queue.mask }} 0xffffffff {% endif %} weight {{ queue.weight }}{%
if queue.codel_enable|default('0') == '1' %} codel {%
if queue.codel_target|default('') != ''%} target {{queue.codel_target}} {% endif %}{%
if queue.codel_interval|default('') != ''%} interval {{queue.codel_interval}} {% endif %}{%
if queue.codel_ecn_enable|default('0') == '1'%} ecn {% else %} noecn {% endif %}{%
elif queue.pie_enable|default('0') == '1' %} pie
{% endif %}
{% endif %}
{% endfor %}
{% endif %}

View File

@ -0,0 +1,12 @@
{% set isEnabled=[] %}
{% if helpers.exists('OPNsense.TrafficShaper.pipes.pipe') %}
{% for pipe in helpers.toList('OPNsense.TrafficShaper.pipes.pipe') %}
{% if pipe.enabled|default('0') == '1' %}
{% do isEnabled.append(pipe) %}
{% endif %}
{% endfor %}
{% endif %}
dummynet_enable="YES"
dnctl_enable="{%if isEnabled %}YES{% else %}NO{% endif %}"
dnctl_rules="/usr/local/etc/dnctl.conf"
dnctl_setup="/usr/local/opnsense/scripts/shaper/setup.sh"