diff --git a/src/etc/inc/shaper.inc b/src/etc/inc/shaper.inc index 250386bf64..459d53c77c 100644 --- a/src/etc/inc/shaper.inc +++ b/src/etc/inc/shaper.inc @@ -31,6 +31,200 @@ require_once("util.inc"); require_once("notices.inc"); include_once("interfaces.inc");//required for convert_real_interface_to_friendly_interface_name() in get_queue_stats() to get altq interface name. +/* Limiter patch */ +// I need this crazy looking model/struct to specify various algos, ecn caps, and params. +// update as needed when dummynet changes + +// List of schedulers ('type' command on scheduler/pipe) that dummynet/ipfw is supposed to support +function getSchedulers() { + return array( + "fifo" => array( + "name" => "FIFO", + "parameter_format" => "type fifo", + "description" => "First-in-First-out is a fundamental queueing discipline which ensures packet sequence.", + "parameters" => array(), + "ecn" => false + ), + "qfq" => array( + "name" => "Quick Fair Queueing", + "parameter_format" => "type qfq", + "description" => "QFQ is a fast, low-complexity Approximated Fair Queueing scheduler.", + "parameters" => array(), + "ecn" => false + ), + "rr" => array( + "name" => "Round Robin", + "parameter_format" => "type rr", + "description" => "Round Robin (RR) schedules packets in a round-robin fashion.", + "parameters" => array(), + "ecn" => false + ), + "wf2q+" => array( + "name" => "Worst-case Weighted fair Queueing", + "parameter_format" => "type wf2q+", + "description" => "Worst-case Weighted fair Queueing (WF2Q+) schedules flows with an associated weight.", + "parameters" => array(), + "ecn" => false + ), + "prio" => array( + "name" => "PRIO", + "parameter_format" => "type prio", + "description" => "PRIO schedules packets by their corresponding priority.", + "parameters" => array(), + "ecn" => false + ), + "fq_codel" => array( + "name" => "FQ_CODEL", + "parameter_format" => "type fq_codel target %sms interval %sms quantum %s limit %s flows %s", + "description" => "CoDel is a novel \"no knobs\", \"just works\", \"handles variable bandwidth and RTT\", and simple AQM algorithm." + . " As a scheduler, FQ_CODEL implements several flows (defined below), each having their own CoDel AQM.", + "parameters" => array( + "target" => array("name" => "Target Delay (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqcodel.target") / 1000), + "interval" => array("name" => "Interval (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqcodel.interval") / 1000), + "quantum" => array("name" => "quantum", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqcodel.quantum")), + "limit" => array("name" => "limit", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqcodel.limit")), + "flows" => array("name" => "flows", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqcodel.flows")) + ), + "ecn" => true + ), + "fq_pie" => array( + "name" => "FQ_PIE", + "parameter_format" => "type fq_pie target %sms tupdate %sms alpha %s beta %s max_burst %s max_ecnth %s", + "description" => "Fair Queue Proportional Integral controller Enhanced AQM (FQ_PIE) is similar to FQ_CODEL but uses a slightly different algorithm.", + "parameters" => array( + "target" => array("name" => "Target Delay (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.target") / 1000), + "tupdate" => array("name" => "Interval (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.tupdate") / 1000), + "alpha" => array("name" => "alpha", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.alpha")), + "beta" => array("name" => "beta", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.beta")), + "max_burst" => array("name" => "max_burst", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.max_burst")), + "max_ecnth" => array("name" => "max_ecnth", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.max_ecnth")) + ), + "ecn" => true + ) + ); +} +// list of AQMs that dummynet supports +function getAQMs() { + return array( + "droptail" => array( + "name" => "Tail Drop", + "description" => "Tail Drop is a fundamental queue management algorithm which drops inbound packets once the queue is full.", + "parameter_format" => "droptail", + "parameters" => array(), + "ecn" => false + ), + "codel" => array( + "name" => "CoDel", + "description" => "CoDel is a novel \"no knobs\", \"just works\", \"handles variable bandwidth and RTT\", and simple AQM algorithm.", + "parameter_format" => "codel target %sms interval %sms", + "parameters" => array( + "target" => array("name" => "Target Delay (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.codel.target") / 1000), + "interval" => array("name" => "Interval (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.codel.interval") / 1000), + ), + "ecn" => true + ), + "pie" => array( + "name" => "PIE", + "description" => "Proportional Integral controller Enhanced AQM (PIE) is similar to CoDel but uses a slightly different algorithm.", + "parameter_format" => "pie target %sms tupdate %sms alpha %s beta %s max_burst %s max_ecnth %s", + "parameters" => array( + "target" => array("name" => "Target Delay (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.target") / 1000), + "tupdate" => array("name" => "Interval (ms)", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.tupdate") / 1000), + "alpha" => array("name" => "alpha", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.alpha")), + "beta" => array("name" => "beta", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.beta")), + "max_burst" => array("name" => "max_burst", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.max_burst")), + "max_ecnth" => array("name" => "max_ecnth", "type" => "number", "default" => get_single_sysctl("net.inet.ip.dummynet.fqpie.max_ecnth")) + ), + "ecn" => true + ), + "red" => array( + "name" => "Random Early Detection (RED)", + "description" => "Random Early Detection (RED) drops packets based on probability, which increases as the queue increases in size.", + "parameter_format" => "red %s/%s/%s/%s", + "parameters" => array( + "w_q" => array("name" => "w_q", "type" => "number", "default" => 1), + "min_th" => array("name" => "min_th", "type" => "number", "default" => 0), + "max_th" => array("name" => "max_th", "type" => "number", "default" => 1), + "max_p" => array("name" => "max_p", "type" => "number", "default" => 1) + ), + "ecn" => true + ), + "gred" => array( + "name" => "Gentle Random Early Detection (GRED)", + "description" => "Gentle Random Early Detection (GRED) drops packets based on probability, which increases as the queue increases in size.", + "parameter_format" => "gred %s/%s/%s/%s", + "parameters" => array( + "w_q" => array("name" => "w_q", "type" => "number", "default" => 1), + "min_th" => array("name" => "min_th", "type" => "number", "default" => 0), + "max_th" => array("name" => "max_th", "type" => "number", "default" => 1), + "max_p" => array("name" => "max_p", "type" => "number", "default" => 1) + ), + "ecn" => true + ) + ); +} +// used to map above +function array_map_assoc(callable $f, array $a) { + return array_column(array_map($f, array_keys($a), $a), 1, 0); +} +function build_queue_params($array, $selected, $params, $id) { + $form .= '
'; + + $selectedAlgorithm = $array[$selected]; + + if ($selectedAlgorithm) { + $form .= '' . gettext($selectedAlgorithm['description']) . '
'; + } + + $parameters = $selectedAlgorithm["parameters"]; + + if (!$parameters || sizeof($parameters) <= 0) { + if (!$selectedAlgorithm) { + $form .= 'No parameters for selected algorithm.'; + } else { + $form .= 'No parameters for ' . $selectedAlgorithm["name"] . '.'; + } + } else { + $form .= '
'; + $form .= ''; + $form .= ""; + $form .= ""; + $form .= ""; + $form .= ""; + $form .= ""; + + foreach ($parameters as $key => $value) { + $form .= ''; + + // get current value, or default. + $currentValue = $params[$key]; + if (!$currentValue || $currentValue == '') { + $currentValue = $value["default"]; // default to default + } + + $form .= ""; + $form .= ""; + + $form .= ''; + } + + $form .= "
ParameterValue
" . htmlspecialchars($key) . "

"; + } + + $form .= '
'; + $form .= ''; + + return($form); +} +function FormatParameters($format_string, $params) { + return vsprintf($format_string, $params); +} +/* End limiter patch */ + function get_queue_stats() { // due to sysutils\qstats not providing accurate stats with 'currently' valid numbers // in its current implementation this php function does the job for now.. @@ -3459,6 +3653,56 @@ class dnpipe_class extends dummynet_class { var $delay; var $qbandwidth = array(); var $qbandwidthtype; + + /* Limiter queue patch */ + var $ecn; // ecn 'on' or 'off' + var $aqm; // key to aqm_map + var $aqm_params = array(); // AQM params + var $scheduler; // key to scheduler_map + var $scheduler_params = array(); // AQM params + function GetAQM() { + return $this->aqm; + } + function SetAQM($aqm) { + $this->aqm = $aqm; + } + function GetAQMParameters() { + return $this->aqm_params; + } + function GetAQMParameter($parameter) { + return $this->aqm_params[$parameter]; + } + function SetAQMParameter($key, $value) { + return $this->aqm_params[$key] = $value; + } + function SetAQMParameters($params) { + $this->aqm_params = $params; + } + function GetECN() { + return $this->ecn; + } + function SetECN($ecn) { + $this->ecn = $ecn; + } + function GetScheduler() { + return $this->scheduler; + } + function SetScheduler($scheduler) { + $this->scheduler = $scheduler; + } + function GetSchedulerParameters() { + return $this->scheduler_params; + } + function SetSchedulerParameters($params) { + $this->scheduler_params = $params; + } + function SetSchedulerParameter($key, $value) { + $this->scheduler_params[$key] = $value; + } + function GetSchedulerParameter($key) { + return $this->scheduler_params[$key]; + } + /* End limiter queue patch */ /* This is here to help on form building and building rules/lists */ var $subqueues = array(); @@ -3479,6 +3723,7 @@ class dnpipe_class extends dummynet_class { } unset_dn_object_by_reference($this->GetLink()); @pfSense_ipfw_pipe("pipe delete " . $this->GetNumber()); + @pfSense_ipfw_pipe("sched delete " . $this->GetNumber()); } function GetBandwidth() { return $this->qbandwidth; @@ -3590,6 +3835,20 @@ class dnpipe_class extends dummynet_class { if ($data['delay'] && (!is_numeric($data['delay']))) { $input_errors[] = gettext("Delay must be an integer."); } + + /* Limiter patch */ + $selectedScheduler = getSchedulers()[$data['sched']]; + if (!$selectedScheduler) { + $input_errors[] = gettext("Selected scheduler not recognized."); + } + $selectedAqm = getAQMs()[$data['aqm']]; + if (!$selectedAqm) { + $input_errors[] = gettext("Selected AQM not recognized."); + } + if ($data['ecn'] && $data['ecn'] == 'on' && !$selectedScheduler["ecn"] && !$selectedAqm["ecn"]) { + $input_errors[] = gettext("Explicit Congestion Notification is selected, but neither " . $selectedAqm["name"] . " nor " . $selectedScheduler["name"] . " support it."); + } + /* End limiter patch */ } function ReadConfig(&$q) { @@ -3669,7 +3928,48 @@ class dnpipe_class extends dummynet_class { $this->SetDescription(""); } $this->SetEnabled($q['enabled']); - + + /* Limiter patch */ + if (isset($q['aqm']) && $q['aqm'] <> "") { + $this->SetAQM($q['aqm']); + } else { + $this->SetAQM('droptail'); + } + // parse AQM arguments + $aqm_map = getAQMs(); + $selectedParameters = $aqm_map[$this->getAQM()]["parameters"]; + if (is_array($selectedParameters)) { + foreach ($selectedParameters as $key => $value) { + $config_key = 'param_' . $this->GetAQM() . '_' . $key; + if (isset($q[$config_key]) && $q[$config_key] <> "") { + $this->SetAQMParameter($key, $q[$config_key]); + } else { + $this->SetAQMParameter($key, $value["default"]); + } + } + } + + if (isset($q['sched']) && $q['sched'] <> "") { + $this->SetScheduler($q['sched']); + } else { + $this->SetScheduler('fifo'); + } + $scheduler_map = getSchedulers(); + $selectedParameters = $scheduler_map[$this->getScheduler()]["parameters"]; + if (is_array($selectedParameters)) { + foreach ($selectedParameters as $key => $value) { + $config_key = 'param_' . $this->GetScheduler() . '_' . $key; + if (isset($q[$config_key]) && $q[$config_key] <> "") { + $this->SetSchedulerParameter($key, $q[$config_key]); + } else { + $this->SetSchedulerParameter($key, $value["default"]); + } + } + } + + // ecn flag + $this->SetECN($q['ecn']); + /* End limiter patch */ } function build_tree() { @@ -3751,13 +4051,38 @@ class dnpipe_class extends dummynet_class { } $this->build_mask_rules($pfq_rule); + /* Limiter patch */ + $selectedAQM = getAQMs()[$this->getAQM()]; + if ($selectedAQM) { + $pfq_rule .= " " . FormatParameters($selectedAQM["parameter_format"], $this->GetAQMParameters()); + if ($selectedAQM["ecn"]) { + if ($this->getECN() == 'on') { + $pfq_rule .= ' ecn'; + } else { + $pfq_rule .= ' noecn'; + } + } + } + $selectedScheduler = getSchedulers()[$this->getScheduler()]; + if ($selectedScheduler) { + $pfq_rule .= "\nsched ". $this->GetNumber() . " config pipe " . $this->GetNumber() . " " . FormatParameters($selectedScheduler["parameter_format"], $this->GetSchedulerParameters()); + if ($selectedScheduler["ecn"]) { + if ($this->getECN() == 'on') { + $pfq_rule .= ' ecn'; + } else { + $pfq_rule .= ' noecn'; + } + } + } $pfq_rule .= "\n"; - + /* End patch */ + if (!empty($this->subqueues) && count($this->subqueues) > 0) { foreach ($this->subqueues as $q) { $pfq_rule .= $q->build_rules(); } } + $pfq_rule .= " \n"; return $pfq_rule; @@ -4010,6 +4335,60 @@ EOD; $sform->add($section); + /* Begin limiter patch */ + $aqm_map = getAQMs(); + $scheduler_map = getSchedulers(); + + $section = new Form_Section('Queue'); + $section->addInput(new Form_Select( + 'aqm', + 'Queue Management Algorithm', + $this->GetAQM(), + array_map_assoc(function ($k, $v) { + return [$k, $v["name"]]; + }, $aqm_map) + ))->setHelp('Active queue management (AQM) is the intelligent drop of network packets inside the limiter, ' . + 'when it becomes full or gets close to becoming full, with the goal of reducing ' . + 'network congestion.'); + + $section->addInput(new Form_StaticText( + '', + build_queue_params($aqm_map, $this->GetAQM(), $this->GetAQMParameters(), "aqm") + ))->setHelp('Specifies the queue management algorithm parameters.'); + + $section->addInput(new Form_Select( + 'sched', + 'Scheduler', + $this->GetScheduler(), + array_map_assoc(function ($k, $v) { + return [$k, $v["name"]]; + }, $scheduler_map) + ))->setHelp('The scheduler manages the sequence of network packets in the limiter\'s queue.'); + + $section->addInput(new Form_StaticText( + '', + build_queue_params($scheduler_map, $this->GetScheduler(), $this->GetSchedulerParameters(), "sched") + ))->setHelp('Specifies the scheduler parameters.'); + + $section->addInput(new Form_Input( + 'qlimit', + 'Queue length', + 'number', + $this->GetQlimit() + ))->setHelp('Specifies the length of the limiter\'s queue, which the scheduler and AQM are responsible for. ' . + 'This field may be left empty.'); + + $section->addInput(new Form_Checkbox( + 'ecn', + 'ECN', + 'Enable Explicit Congestion Notification (ECN)', + ($this->GetECN() == "on"), + 'on' + ))->setHelp('ECN sets a reserved TCP flag when the queue is nearing or exceeding capacity. Not all AQMs or schedulers support this.'); + + $sform->add($section); + /* End limiter patch */ + $section = new Form_Section('Advanced Options'); $section->addInput(new Form_Input( @@ -4028,14 +4407,6 @@ EOD; ))->setHelp('In most cases, zero (0) should be specified here (or leave the field empty). ' . 'A value of 0.001 means one packet in 1000 gets dropped.'); - $section->addInput(new Form_Input( - 'qlimit', - 'Queue size (slots)', - 'number', - $this->GetQlimit() - ))->setHelp('In most cases, the field should be left empty. All packets in this pipe are placed into a fixed-size queue first, ' . - 'then they are delayed by value specified in the Delay field, and then they are delivered to their destination.'); - $section->addInput(new Form_Input( 'buckets', 'Bucket size (slots)', @@ -4075,6 +4446,24 @@ EOD; $cflink['maskbits'] = $mask['bits']; $cflink['maskbitsv6'] = $mask['bitsv6']; $cflink['delay'] = $this->GetDelay(); + + /* Limiter queue patch */ + $cflink['sched'] = $this->GetScheduler(); + $scheduler_map = getSchedulers(); + $selectedParameters = $scheduler_map[$this->getScheduler()]["parameters"]; + foreach ($selectedParameters as $key => $value) { + $config_key = 'param_' . $this->GetScheduler() . '_' . $key; + $cflink[$config_key] = $this->GetSchedulerParameter($key); + } + $cflink['aqm'] = $this->GetAQM(); + $aqm_map = GetAQMs(); + $selectedParameters = $aqm_map[$this->getAQM()]["parameters"]; + foreach ($selectedParameters as $key => $value) { + $config_key = 'param_' . $this->GetAQM() . '_' . $key; + $cflink[$config_key] = $this->GetAQMParameter($key); + } + $cflink['ecn'] = $this->GetECN(); + /* End limiter queue patch */ } } @@ -4082,6 +4471,35 @@ EOD; class dnqueue_class extends dummynet_class { var $pipeparent; var $weight; + /* Limiter queue patch */ + var $ecn; // ecn 'on' or 'off' + var $aqm; // key to aqm_map + var $aqm_params = array(); // AQM params + function GetAQM() { + return $this->aqm; + } + function SetAQM($aqm) { + $this->aqm = $aqm; + } + function GetAQMParameters() { + return $this->aqm_params; + } + function GetAQMParameter($parameter) { + return $this->aqm_params[$parameter]; + } + function SetAQMParameter($key, $value) { + return $this->aqm_params[$key] = $value; + } + function SetAQMParameters($params) { + $this->aqm_params = $params; + } + function GetECN() { + return $this->ecn; + } + function SetECN($ecn) { + $this->ecn = $ecn; + } + /* End limiter queue patch */ function GetWeight() { return $this->weight; @@ -4109,6 +4527,17 @@ class dnqueue_class extends dummynet_class { function validate_input($data, &$input_errors) { parent::validate_input($data, $input_errors); + + + /* Limiter patch */ + $selectedAqm = getAQMs()[$data['aqm']]; + if (!$selectedAqm) { + $input_errors[] = gettext("Selected AQM not recognized."); + } + if ($data['ecn'] && $data['ecn'] == 'on' && !$selectedAqm["ecn"]) { + $input_errors[] = gettext("Explicit Congestion Notification is selected, but " . $selectedAqm["name"] . " does not support it."); + } + /* End limiter patch */ if ($data['weight'] && ((!is_numeric($data['weight'])) || ($data['weight'] < 1 && $data['weight'] > 100))) { @@ -4189,6 +4618,28 @@ class dnqueue_class extends dummynet_class { $this->SetDescription(""); } $this->SetEnabled($q['enabled']); + + /* Limiter patch */ + if (isset($q['aqm']) && $q['aqm'] <> "") { + $this->SetAQM($q['aqm']); + } else { + $this->SetAQM('droptail'); + } + // parse AQM arguments + $aqm_map = getAQMs(); + $selectedParameters = $aqm_map[$this->getAQM()]["parameters"]; + foreach ($selectedParameters as $key => $value) { + $config_key = 'param_' . $this->GetAQM() . '_' . $key; + if (isset($q[$config_key]) && $q[$config_key] <> "") { + $this->SetAQMParameter($key, $q[$config_key]); + } else { + $this->SetAQMParameter($key, $value["default"]); + } + } + + // ecn flag + $this->SetECN($q['ecn']); + /* End limiter patch */ } function build_tree() { @@ -4217,6 +4668,21 @@ class dnqueue_class extends dummynet_class { $pfq_rule .= " buckets " . $this->GetBuckets(); } $this->build_mask_rules($pfq_rule); + + /* Limiter patch */ + $selectedAQM = getAQMs()[$this->getAQM()]; + if ($selectedAQM) { + $pfq_rule .= " " . FormatParameters($selectedAQM["parameter_format"], $this->GetAQMParameters()); + if ($selectedAQM["ecn"]) { + if ($this->getECN() == 'on') { + $pfq_rule .= ' ecn'; + } else { + $pfq_rule .= ' noecn'; + } + } + } + /* End patch */ + $pfq_rule .= "\n"; return $pfq_rule; @@ -4313,6 +4779,44 @@ class dnqueue_class extends dummynet_class { ))->setHelp('A description may be entered here for administrative reference (not parsed).'); $sform->add($section); + + /* Begin limiter patch */ + $aqm_map = getAQMs(); + + $section = new Form_Section('Queue'); + $section->addInput(new Form_Select( + 'aqm', + 'Queue Management Algorithm', + $this->GetAQM(), + array_map_assoc(function ($k, $v) { + return [$k, $v["name"]]; + }, $aqm_map) + ))->setHelp('Active queue management (AQM) is the intelligent drop of network packets inside this limiter\'s queue, ' . + 'when it becomes full or gets close to becoming full, with the goal of reducing ' . + 'network congestion.'); + + $section->addInput(new Form_StaticText( + '', + build_queue_params($aqm_map, $this->GetAQM(), $this->GetAQMParameters(), "aqm") + ))->setHelp('Specifies the queue management algorithm parameters.'); + + $section->addInput(new Form_Input( + 'qlimit', + 'Queue length', + 'number', + $this->GetQlimit() + ))->setHelp('Specifies the length of this queue, which the AQM is responsible for. This field may be left empty.'); + + $section->addInput(new Form_Checkbox( + 'ecn', + 'ECN', + 'Enable Explicit Congestion Notification (ECN)', + ($this->GetECN() == "on"), + 'on' + ))->setHelp('ECN sets a reserved TCP flag when the queue is nearing or exceeding capacity. Not all AQMs or schedulers support this.'); + + $sform->add($section); + /* End limiter patch */ $section = new Form_Section('Advanced Options'); @@ -4333,15 +4837,7 @@ class dnqueue_class extends dummynet_class { ['step' => '0.001', 'min' => '0.000'] ))->setHelp('In most cases, zero (0) should be specified here (or leave the field empty). ' . 'A value of 0.001 means one packet in 1000 gets dropped'); - - $section->addInput(new Form_Input( - 'qlimit', - 'Queue size (slots)', - 'number', - $this->GetQlimit() - ))->setHelp('In most cases, the field should be left empty. All packets in this pipe are placed into a fixed-size queue first, ' . - 'then they are delayed by value specified in the Delay field, and then they are delivered to their destination.'); - + $section->addInput(new Form_Input( 'buckets', 'Bucket size (slots)', @@ -4381,6 +4877,17 @@ class dnqueue_class extends dummynet_class { $cflink['mask'] = $mask['type']; $cflink['maskbits'] = $mask['bits']; $cflink['maskbitsv6'] = $mask['bitsv6']; + + /* Limiter queue patch */ + $cflink['aqm'] = $this->GetAQM(); + $aqm_map = GetAQMs(); + $selectedParameters = $aqm_map[$this->getAQM()]["parameters"]; + foreach ($selectedParameters as $key => $value) { + $config_key = 'param_' . $this->GetAQM() . '_' . $key; + $cflink[$config_key] = $this->GetAQMParameter($key); + } + $cflink['ecn'] = $this->GetECN(); + /* End limiter queue patch */ } } @@ -4776,4 +5283,4 @@ function interface_has_queue($if) { return false; } -?> +?> \ No newline at end of file