diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/qos/__init__.py | 28 | ||||
| -rw-r--r-- | python/vyos/qos/base.py | 276 | ||||
| -rw-r--r-- | python/vyos/qos/cake.py | 55 | ||||
| -rw-r--r-- | python/vyos/qos/droptail.py | 28 | ||||
| -rw-r--r-- | python/vyos/qos/fairqueue.py | 31 | ||||
| -rw-r--r-- | python/vyos/qos/fqcodel.py | 40 | ||||
| -rw-r--r-- | python/vyos/qos/limiter.py | 27 | ||||
| -rw-r--r-- | python/vyos/qos/netem.py | 53 | ||||
| -rw-r--r-- | python/vyos/qos/priority.py | 40 | ||||
| -rw-r--r-- | python/vyos/qos/randomdetect.py | 54 | ||||
| -rw-r--r-- | python/vyos/qos/ratelimiter.py | 37 | ||||
| -rw-r--r-- | python/vyos/qos/roundrobin.py | 44 | ||||
| -rw-r--r-- | python/vyos/qos/trafficshaper.py | 104 | 
13 files changed, 817 insertions, 0 deletions
| diff --git a/python/vyos/qos/__init__.py b/python/vyos/qos/__init__.py new file mode 100644 index 000000000..a2980ccde --- /dev/null +++ b/python/vyos/qos/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase +from vyos.qos.cake import CAKE +from vyos.qos.droptail import DropTail +from vyos.qos.fairqueue import FairQueue +from vyos.qos.fqcodel import FQCodel +from vyos.qos.limiter import Limiter +from vyos.qos.netem import NetEm +from vyos.qos.priority import Priority +from vyos.qos.randomdetect import RandomDetect +from vyos.qos.ratelimiter import RateLimiter +from vyos.qos.roundrobin import RoundRobin +from vyos.qos.trafficshaper import TrafficShaper +from vyos.qos.trafficshaper import TrafficShaperHFSC diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py new file mode 100644 index 000000000..d039bbb0f --- /dev/null +++ b/python/vyos/qos/base.py @@ -0,0 +1,276 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.base import Warning +from vyos.util import cmd +from vyos.util import dict_search +from vyos.util import read_file + +class QoSBase: +    _debug = True +    _direction = ['egress'] +    _parent = 0xffff + +    def __init__(self, interface): +        self._interface = interface + +    def _cmd(self, command): +        if self._debug: +            print(f'DEBUG/QoS: {command}') +        return cmd(command) + +    def get_direction(self) -> list: +        return self._direction + +    def _get_class_max_id(self, config) -> int: +        if 'class' in config: +            tmp = list(config['class'].keys()) +            tmp.sort(key=lambda ii: int(ii)) +            return tmp[-1] +        return None + +    def _tmp_qdisc(self, config : dict, cls_id : int): +        """ +        Add/replace qdisc for every class (also default is a class). This is +        a genetic method which need an implementation "per" queue-type. + +        This matches the old mapping as defined in Perl here: +        https://github.com/vyos/vyatta-cfg-qos/blob/equuleus/lib/Vyatta/Qos/ShaperClass.pm#L223-L229 +        """ +        queue_type = dict_search('queue_type', config) +        default_tc = f'tc qdisc replace dev {self._interface} parent {self._parent}:{cls_id:x}' + +        if queue_type == 'priority': +            handle = 0x4000 + cls_id +            default_tc += f' handle {handle:x}: prio' +            self._cmd(default_tc) + +            queue_limit = dict_search('queue_limit', config) +            for ii in range(1, 4): +                tmp = f'tc qdisc replace dev {self._interface} parent {handle:x}:{ii:x} pfifo limit {queue_limit}' +                self._cmd(tmp) + +        elif queue_type == 'fair-queue': +            default_tc += f' sfq' + +            tmp = dict_search('queue_limit', config) +            if tmp: default_tc += f' limit {tmp}' + +            self._cmd(default_tc) + +        elif queue_type == 'fq-codel': +            default_tc += f' fq_codel' +            tmp = dict_search('codel_quantum', config) +            if tmp: default_tc += f' quantum {tmp}' + +            tmp = dict_search('flows', config) +            if tmp: default_tc += f' flows {tmp}' + +            tmp = dict_search('interval', config) +            if tmp: default_tc += f' interval {tmp}' + +            tmp = dict_search('interval', config) +            if tmp: default_tc += f' interval {tmp}' + +            tmp = dict_search('queue_limit', config) +            if tmp: default_tc += f' limit {tmp}' + +            tmp = dict_search('target', config) +            if tmp: default_tc += f' target {tmp}' + +            default_tc += f' noecn' + +            self._cmd(default_tc) + +        elif queue_type == 'random-detect': +            default_tc += f' red' + +            self._cmd(default_tc) + +        elif queue_type == 'drop-tail': +            default_tc += f' pfifo' + +            tmp = dict_search('queue_limit', config) +            if tmp: default_tc += f' limit {tmp}' + +            self._cmd(default_tc) + +    def _rate_convert(self, rate) -> int: +        rates = { +            'bit'   : 1, +            'kbit'  : 1000, +            'mbit'  : 1000000, +            'gbit'  : 1000000000, +            'tbit'  : 1000000000000, +        } + +        if rate == 'auto': +            speed = read_file(f'/sys/class/net/{self._interface}/speed') +            if not speed.isnumeric(): +                Warning('Interface speed cannot be determined (assuming 10 Mbit/s)') +                speed = 10 +            return int(speed) *1000000 # convert to MBit/s + +        rate_numeric = int(''.join([n for n in rate if n.isdigit()])) +        rate_scale   = ''.join([n for n in rate if not n.isdigit()]) + +        if int(rate_numeric) <= 0: +            raise ValueError(f'{rate_numeric} is not a valid bandwidth <= 0') + +        if rate_scale: +            return int(rate_numeric * rates[rate_scale]) +        else: +            # No suffix implies Kbps just as Cisco IOS +            return int(rate_numeric * 1000) + +    def update(self, config, direction, priority=None): +        """ method must be called from derived class after it has completed qdisc setup """ + +        if 'class' in config: +            for cls, cls_config in config['class'].items(): + +                self._tmp_qdisc(cls_config, int(cls)) + +                if 'match' in cls_config: +                    for match, match_config in cls_config['match'].items(): +                        for af in ['ip', 'ipv6']: +                            # every match criteria has it's tc instance +                            filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}:' + +                            if priority: +                                filter_cmd += f' prio {cls}' +                            elif 'priority' in cls_config: +                                prio = cls_config['priority'] +                                filter_cmd += f' prio {prio}' + +                            filter_cmd += ' protocol all u32' + +                            tc_af = af +                            if af == 'ipv6': +                                tc_af = 'ip6' + +                            if af in match_config: +                                tmp = dict_search(f'{af}.source.address', match_config) +                                if tmp: filter_cmd += f' match {tc_af} src {tmp}' + +                                tmp = dict_search(f'{af}.source.port', match_config) +                                if tmp: filter_cmd += f' match {tc_af} sport {tmp} 0xffff' + +                                tmp = dict_search(f'{af}.destination.address', match_config) +                                if tmp: filter_cmd += f' match {tc_af} dst {tmp}' + +                                tmp = dict_search(f'{af}.destination.port', match_config) +                                if tmp: filter_cmd += f' match {tc_af} dport {tmp} 0xffff' + +                                tmp = dict_search(f'{af}.protocol', match_config) +                                if tmp: filter_cmd += f' match {tc_af} protocol {tmp} 0xff' + +                                # Will match against total length of an IPv4 packet and +                                # payload length of an IPv6 packet. +                                # +                                # IPv4 : match u16 0x0000 ~MAXLEN at 2 +                                # IPv6 : match u16 0x0000 ~MAXLEN at 4 +                                tmp = dict_search(f'{af}.max_length', match_config) +                                if tmp: +                                    # We need the 16 bit two's complement of the maximum +                                    # packet length +                                    tmp = hex(0xffff & ~int(tmp)) + +                                    if af == 'ip': +                                        filter_cmd += f' match u16 0x0000 {tmp} at 2' +                                    elif af == 'ipv6': +                                        filter_cmd += f' match u16 0x0000 {tmp} at 4' + +                                # We match against specific TCP flags - we assume the IPv4 +                                # header length is 20 bytes and assume the IPv6 packet is +                                # not using extension headers (hence a ip header length of 40 bytes) +                                # TCP Flags are set on byte 13 of the TCP header. +                                # IPv4 : match u8 X X at 33 +                                # IPv6 : match u8 X X at 53 +                                # with X = 0x02 for SYN and X = 0x10 for ACK +                                tmp = dict_search(f'{af}.tcp', match_config) +                                if tmp: +                                    mask = 0 +                                    if 'ack' in tmp: +                                        mask |= 0x10 +                                    if 'syn' in tmp: +                                        mask |= 0x02 +                                    mask = hex(mask) + +                                    if af == 'ip': +                                        filter_cmd += f' match u8 {mask} {mask} at 33' +                                    elif af == 'ipv6': +                                        filter_cmd += f' match u8 {mask} {mask} at 53' + +                                # The police block allows limiting of the byte or packet rate of +                                # traffic matched by the filter it is attached to. +                                # https://man7.org/linux/man-pages/man8/tc-police.8.html +                                if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config): +                                    filter_cmd += f' action police' + +                                if 'exceed' in cls_config: +                                    action = cls_config['exceed'] +                                    filter_cmd += f' conform-exceed {action}' +                                    if 'not_exceed' in cls_config: +                                        action = cls_config['not_exceed'] +                                        filter_cmd += f'/{action}' + +                                if 'bandwidth' in cls_config: +                                    rate = self._rate_convert(cls_config['bandwidth']) +                                    filter_cmd += f' rate {rate}' + +                                if 'burst' in cls_config: +                                    burst = cls_config['burst'] +                                    filter_cmd += f' burst {burst}' + +                                cls = int(cls) +                                filter_cmd += f' flowid {self._parent:x}:{cls:x}' +                                self._cmd(filter_cmd) + +        if 'default' in config: +            class_id_max = self._get_class_max_id(config) +            default_cls_id = int(class_id_max) +1 + +            if 'default' in config: +                self._tmp_qdisc(config['default'], default_cls_id) + +            filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}: ' +            filter_cmd += 'prio 255 protocol all basic' + +            # The police block allows limiting of the byte or packet rate of +            # traffic matched by the filter it is attached to. +            # https://man7.org/linux/man-pages/man8/tc-police.8.html +            if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config): +                filter_cmd += f' action police' + +            if 'exceed' in cls_config: +                action = cls_config['exceed'] +                filter_cmd += f' conform-exceed {action}' +                if 'not_exceed' in cls_config: +                    action = cls_config['not_exceed'] +                    filter_cmd += f'/{action}' + +            if 'bandwidth' in cls_config: +                rate = self._rate_convert(cls_config['bandwidth']) +                filter_cmd += f' rate {rate}' + +            if 'burst' in cls_config: +                burst = cls_config['burst'] +                filter_cmd += f' burst {burst}' + + +            filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}' +            self._cmd(filter_cmd) + diff --git a/python/vyos/qos/cake.py b/python/vyos/qos/cake.py new file mode 100644 index 000000000..a89b1de1e --- /dev/null +++ b/python/vyos/qos/cake.py @@ -0,0 +1,55 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class CAKE(QoSBase): +    _direction = ['egress'] + +    # https://man7.org/linux/man-pages/man8/tc-cake.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root handle 1: cake {direction}' +        if 'bandwidth' in config: +            bandwidth = self._rate_convert(config['bandwidth']) +            tmp += f' bandwidth {bandwidth}' + +        if 'rtt' in config: +            rtt = config['rtt'] +            tmp += f' rtt {rtt}ms' + +        if 'flow_isolation' in config: +            if 'blind' in config['flow_isolation']: +                tmp += f' flowblind' +            if 'dst_host' in config['flow_isolation']: +                tmp += f' dsthost' +            if 'dual_dst_host' in config['flow_isolation']: +                tmp += f' dual-dsthost' +            if 'dual_src_host' in config['flow_isolation']: +                tmp += f' dual-srchost' +            if 'flow' in config['flow_isolation']: +                tmp += f' flows' +            if 'host' in config['flow_isolation']: +                tmp += f' hosts' +            if 'nat' in config['flow_isolation']: +                tmp += f' nat' +            if 'src_host' in config['flow_isolation']: +                tmp += f' srchost ' +        else: +            tmp += f' nonat' + +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/droptail.py b/python/vyos/qos/droptail.py new file mode 100644 index 000000000..427d43d19 --- /dev/null +++ b/python/vyos/qos/droptail.py @@ -0,0 +1,28 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class DropTail(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-pfifo.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root pfifo' +        if 'queue_limit' in config: +            limit = config["queue_limit"] +            tmp += f' limit {limit}' +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/fairqueue.py b/python/vyos/qos/fairqueue.py new file mode 100644 index 000000000..f41d098fb --- /dev/null +++ b/python/vyos/qos/fairqueue.py @@ -0,0 +1,31 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class FairQueue(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-sfq.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root sfq' + +        if 'hash_interval' in config: +            tmp += f' perturb {config["hash_interval"]}' +        if 'queue_limit' in config: +            tmp += f' limit {config["queue_limit"]}' + +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/fqcodel.py b/python/vyos/qos/fqcodel.py new file mode 100644 index 000000000..cd2340aa2 --- /dev/null +++ b/python/vyos/qos/fqcodel.py @@ -0,0 +1,40 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class FQCodel(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-fq_codel.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root fq_codel' + +        if 'codel_quantum' in config: +            tmp += f' quantum {config["codel_quantum"]}' +        if 'flows' in config: +            tmp += f' flows {config["flows"]}' +        if 'interval' in config: +            interval = int(config['interval']) * 1000 +            tmp += f' interval {interval}' +        if 'queue_limit' in config: +            tmp += f' limit {config["queue_limit"]}' +        if 'target' in config: +            target = int(config['target']) * 1000 +            tmp += f' target {target}' + +        tmp += f' noecn' +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/limiter.py b/python/vyos/qos/limiter.py new file mode 100644 index 000000000..ace0c0b6c --- /dev/null +++ b/python/vyos/qos/limiter.py @@ -0,0 +1,27 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class Limiter(QoSBase): +    _direction = ['ingress'] + +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} handle {self._parent:x}: {direction}' +        self._cmd(tmp) + +        # base class must be called last +        super().update(config, direction) + diff --git a/python/vyos/qos/netem.py b/python/vyos/qos/netem.py new file mode 100644 index 000000000..8bdef300b --- /dev/null +++ b/python/vyos/qos/netem.py @@ -0,0 +1,53 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class NetEm(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-netem.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root netem' +        if 'bandwidth' in config: +            rate = self._rate_convert(config["bandwidth"]) +            tmp += f' rate {rate}' + +        if 'queue_limit' in config: +            limit = config["queue_limit"] +            tmp += f' limit {limit}' + +        if 'delay' in config: +            delay = config["delay"] +            tmp += f' delay {delay}ms' + +        if 'loss' in config: +            drop  = config["loss"] +            tmp += f' drop {drop}%' + +        if 'corruption' in config: +            corrupt = config["corruption"] +            tmp += f' corrupt {corrupt}%' + +        if 'reordering' in config: +            reorder = config["reordering"] +            tmp += f' reorder {reorder}%' + +        if 'duplicate' in config: +            duplicate = config["duplicate"] +            tmp += f' duplicate {duplicate}%' + +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/priority.py b/python/vyos/qos/priority.py new file mode 100644 index 000000000..72092b7ef --- /dev/null +++ b/python/vyos/qos/priority.py @@ -0,0 +1,40 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase +from vyos.util import dict_search + +class Priority(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc-prio.8.html +    def update(self, config, direction): +        if 'class' in config: +            class_id_max = self._get_class_max_id(config) +            bands = int(class_id_max) +1 + +            tmp = f'tc qdisc add dev {self._interface} root handle {self._parent:x}: prio bands {bands} priomap ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' +            self._cmd(tmp) + +            for cls in config['class']: +                tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{cls:x} pfifo' +                self._cmd(tmp) + +        # base class must be called last +        super().update(config, direction, priority=True) diff --git a/python/vyos/qos/randomdetect.py b/python/vyos/qos/randomdetect.py new file mode 100644 index 000000000..d7d84260f --- /dev/null +++ b/python/vyos/qos/randomdetect.py @@ -0,0 +1,54 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class RandomDetect(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc.8.html +    def update(self, config, direction): + +        tmp = f'tc qdisc add dev {self._interface} root handle {self._parent}:0 dsmark indices 8 set_tc_index' +        self._cmd(tmp) + +        tmp = f'tc filter add dev {self._interface} parent {self._parent}:0 protocol ip prio 1 tcindex mask 0xe0 shift 5' +        self._cmd(tmp) + +        # Generalized Random Early Detection +        handle = self._parent +1 +        tmp = f'tc qdisc add dev {self._interface} parent {self._parent}:0 handle {handle}:0 gred setup DPs 8 default 0 grio' +        self._cmd(tmp) + +        bandwidth = self._rate_convert(config['bandwidth']) + +        # set VQ (virtual queue) parameters +        for precedence, precedence_config in config['precedence'].items(): +            precedence = int(precedence) +            avg_pkt = int(precedence_config['average_packet']) +            limit = int(precedence_config['queue_limit']) * avg_pkt +            min_val = int(precedence_config['minimum_threshold']) * avg_pkt +            max_val = int(precedence_config['maximum_threshold']) * avg_pkt + +            tmp  = f'tc qdisc change dev {self._interface} handle {handle}:0 gred limit {limit} min {min_val} max {max_val} avpkt {avg_pkt} ' + +            burst = (2 * int(precedence_config['minimum_threshold']) + int(precedence_config['maximum_threshold'])) // 3 +            probability = 1 / int(precedence_config['mark_probability']) +            tmp += f'burst {burst} bandwidth {bandwidth} probability {probability} DP {precedence} prio {8 - precedence:x}' + +            self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/ratelimiter.py b/python/vyos/qos/ratelimiter.py new file mode 100644 index 000000000..a4f80a1be --- /dev/null +++ b/python/vyos/qos/ratelimiter.py @@ -0,0 +1,37 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class RateLimiter(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-tbf.8.html +    def update(self, config, direction): +        # call base class +        super().update(config, direction) + +        tmp = f'tc qdisc add dev {self._interface} root tbf' +        if 'bandwidth' in config: +            rate = self._rate_convert(config['bandwidth']) +            tmp += f' rate {rate}' + +        if 'burst' in config: +            burst = config['burst'] +            tmp += f' burst {burst}' + +        if 'latency' in config: +            latency = config['latency'] +            tmp += f' latency {latency}ms' + +        self._cmd(tmp) diff --git a/python/vyos/qos/roundrobin.py b/python/vyos/qos/roundrobin.py new file mode 100644 index 000000000..4a0cb18aa --- /dev/null +++ b/python/vyos/qos/roundrobin.py @@ -0,0 +1,44 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class RoundRobin(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc-drr.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root handle 1: drr' +        self._cmd(tmp) + +        if 'class' in config: +            for cls in config['class']: +                cls = int(cls) +                tmp = f'tc class add dev {self._interface} parent 1:1 classid 1:{cls:x} drr' +                self._cmd(tmp) + +                tmp = f'tc qdisc add dev {self._interface} parent 1:{cls:x} pfifo' +                self._cmd(tmp) + +        if 'default' in config: +            class_id_max = self._get_class_max_id(config) +            default_cls_id = int(class_id_max) +1 + +            # class ID via CLI is in range 1-4095, thus 1000 hex = 4096 +            tmp = f'tc class add dev {self._interface} parent 1:1 classid 1:{default_cls_id:x} drr' +            self._cmd(tmp) + +        # call base class +        super().update(config, direction, priority=True) diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py new file mode 100644 index 000000000..6d465b38e --- /dev/null +++ b/python/vyos/qos/trafficshaper.py @@ -0,0 +1,104 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from math import ceil +from vyos.qos.base import QoSBase + +# Kernel limits on quantum (bytes) +MAXQUANTUM = 200000 +MINQUANTUM = 1000 + +class TrafficShaper(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc-htb.8.html +    def update(self, config, direction): +        class_id_max = 0 +        if 'class' in config: +            tmp = list(config['class']) +            tmp.sort() +            class_id_max = tmp[-1] + +        r2q = 10 +        # bandwidth is a mandatory CLI node +        speed = self._rate_convert(config['bandwidth']) +        speed_bps = int(speed) // 8 + +        # need a bigger r2q if going fast than 16 mbits/sec +        if (speed_bps // r2q) >= MAXQUANTUM: # integer division +            r2q = ceil(speed_bps // MAXQUANTUM) +            print(r2q) +        else: +            # if there is a slow class then may need smaller value +            if 'class' in config: +                min_speed = speed_bps +                for cls, cls_options in config['class'].items(): +                    # find class with the lowest bandwidth used +                    if 'bandwidth' in cls_options: +                        bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second +                        if bw_bps < min_speed: +                            min_speed = bw_bps + +                while (r2q > 1) and (min_speed // r2q) < MINQUANTUM: +                    tmp = r2q -1 +                    if (speed_bps // tmp) >= MAXQUANTUM: +                        break +                    r2q = tmp + + +        default_minor_id = int(class_id_max) +1 +        tmp = f'tc qdisc add dev {self._interface} root handle {self._parent:x}: htb r2q {r2q} default {default_minor_id:x}' # default is in hex +        self._cmd(tmp) + +        tmp = f'tc class add dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 htb rate {speed}' +        self._cmd(tmp) + +        if 'class' in config: +            for cls, cls_config in config['class'].items(): +                # class id is used later on and passed as hex, thus this needs to be an int +                cls = int(cls) + +                # bandwidth is a mandatory CLI node +                rate = self._rate_convert(cls_config['bandwidth']) +                burst = cls_config['burst'] +                quantum = cls_config['codel_quantum'] + +                tmp = f'tc class add dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} htb rate {rate} burst {burst} quantum {quantum}' +                if 'priority' in cls_config: +                    priority = cls_config['priority'] +                    tmp += f' prio {priority}' +                self._cmd(tmp) + +                tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{cls:x} sfq' +                self._cmd(tmp) + +        if 'default' in config: +                tmp = f'tc class add dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} htb rate {rate} burst {burst} quantum {quantum}' +                if 'priority' in config['default']: +                    priority = config['default']['priority'] +                    tmp += f' prio {priority}' +                self._cmd(tmp) + +                tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq' +                self._cmd(tmp) + +        # call base class +        super().update(config, direction) + +class TrafficShaperHFSC(TrafficShaper): +    def update(self, config, direction): +        # call base class +        super().update(config, direction) + | 
