diff --git a/plist b/plist index 5d143df17..82fe10ef7 100644 --- a/plist +++ b/plist @@ -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 diff --git a/src/etc/rc.ipfw b/src/etc/rc.ipfw index d31eb6d01..dd81612f9 100755 --- a/src/etc/rc.ipfw +++ b/src/etc/rc.ipfw @@ -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 diff --git a/src/opnsense/mvc/app/controllers/OPNsense/TrafficShaper/Api/ServiceController.php b/src/opnsense/mvc/app/controllers/OPNsense/TrafficShaper/Api/ServiceController.php index 86001e482..5020a7f1a 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/TrafficShaper/Api/ServiceController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/TrafficShaper/Api/ServiceController.php @@ -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(); diff --git a/src/opnsense/scripts/shaper/lib/__init__.py b/src/opnsense/scripts/shaper/lib/__init__.py index c0a893b35..da7728975 100755 --- a/src/opnsense/scripts/shaper/lib/__init__.py +++ b/src/opnsense/scripts/shaper/lib/__init__.py @@ -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: diff --git a/src/opnsense/scripts/shaper/setup.sh b/src/opnsense/scripts/shaper/setup.sh new file mode 100755 index 000000000..83421aa0a --- /dev/null +++ b/src/opnsense/scripts/shaper/setup.sh @@ -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 diff --git a/src/opnsense/service/conf/actions.d/actions_ipfw.conf b/src/opnsense/service/conf/actions.d/actions_ipfw.conf index 3935361da..a920d7d0f 100644 --- a/src/opnsense/service/conf/actions.d/actions_ipfw.conf +++ b/src/opnsense/service/conf/actions.d/actions_ipfw.conf @@ -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 diff --git a/src/opnsense/service/conf/actions.d/actions_shaper.conf b/src/opnsense/service/conf/actions.d/actions_shaper.conf new file mode 100644 index 000000000..e66b4dba2 --- /dev/null +++ b/src/opnsense/service/conf/actions.d/actions_shaper.conf @@ -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 diff --git a/src/opnsense/service/templates/OPNsense/IPFW/ipfw.conf b/src/opnsense/service/templates/OPNsense/IPFW/ipfw.conf index 6e98657bc..767817670 100644 --- a/src/opnsense/service/templates/OPNsense/IPFW/ipfw.conf +++ b/src/opnsense/service/templates/OPNsense/IPFW/ipfw.conf @@ -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 #====================================================================================== diff --git a/src/opnsense/service/templates/OPNsense/IPFW/rc.conf.d b/src/opnsense/service/templates/OPNsense/IPFW/rc.conf.d index 1e0be2bc0..5f153e783 100644 --- a/src/opnsense/service/templates/OPNsense/IPFW/rc.conf.d +++ b/src/opnsense/service/templates/OPNsense/IPFW/rc.conf.d @@ -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" diff --git a/src/opnsense/service/templates/OPNsense/Shaper/+TARGETS b/src/opnsense/service/templates/OPNsense/Shaper/+TARGETS new file mode 100644 index 000000000..44306d7f9 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Shaper/+TARGETS @@ -0,0 +1,2 @@ +rc.conf.d:/etc/rc.conf.d/dnctl +dnctl.conf:/usr/local/etc/dnctl.conf diff --git a/src/opnsense/service/templates/OPNsense/Shaper/dnctl.conf b/src/opnsense/service/templates/OPNsense/Shaper/dnctl.conf new file mode 100644 index 000000000..b9e72dc53 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Shaper/dnctl.conf @@ -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 %} diff --git a/src/opnsense/service/templates/OPNsense/Shaper/rc.conf.d b/src/opnsense/service/templates/OPNsense/Shaper/rc.conf.d new file mode 100644 index 000000000..fdb9a1c70 --- /dev/null +++ b/src/opnsense/service/templates/OPNsense/Shaper/rc.conf.d @@ -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"