tcconfig/_tc_command_helper.py
def get_tc_base_command(tc_subcommand):
    if not isinstance(tc_subcommand, TcSubCommand):
        raise ValueError("the argument must be a TcSubCommand value")

    return "{:s} {:s}".format(find_bin_path("tc"), tc_subcommand.value)
tcconfig/_const.py
class TcSubCommand(enum.Enum):
    CLASS = "class"
    FILTER = "filter"
    QDISC = "qdisc"
tcconfig/shaper/tbf.py
class TbfShaper(AbstractShaper):

    __NETEM_QDISC_MAJOR_ID_OFFSET = 10

    __MIN_BUFFER_BYTE = 1600

    __OUT_DEVICE_QDISC_MINOR_ID = 1
    __IN_DEVICE_QDISC_MINOR_ID = 3

    @property
    def algorithm_name(self):
        return ShapingAlgorithm.TBF

    def _get_qdisc_minor_id(self):
        if self._tc_obj.direction == TrafficDirection.OUTGOING:
            return self.__OUT_DEVICE_QDISC_MINOR_ID

        if self._tc_obj.direction == TrafficDirection.INCOMING:
            return self.__IN_DEVICE_QDISC_MINOR_ID

        raise ParameterError(
            "unknown direction", expected=TrafficDirection.LIST, value=self._tc_obj.direction
        )

    def _get_netem_qdisc_major_id(self, base_id):
        if self._tc_obj.direction == TrafficDirection.OUTGOING:
            direction_offset = 0
        elif self._tc_obj.direction == TrafficDirection.INCOMING:
            direction_offset = 1

        return base_id + self.__NETEM_QDISC_MAJOR_ID_OFFSET + direction_offset

    def _make_qdisc(self):
        base_command = self._tc_obj.get_tc_command(TcSubCommand.QDISC)
        handle = "{:s}:".format(self._tc_obj.qdisc_major_id_str)

        return run_command_helper(
            " ".join([base_command, self._dev, "root", "handle {:s}".format(handle), "prio"]),
            ignore_error_msg_regexp=self._tc_obj.REGEXP_FILE_EXISTS,
            notice_msg=self._tc_obj.EXISTS_MSG_TEMPLATE.format(
                "failed to '{command:s}': prio qdisc already exists "
                "({dev:s}, algo={algorithm:s}, handle={handle:s}).".format(
                    command=base_command,
                    dev=self._dev,
                    algorithm=self.algorithm_name,
                    handle=handle,
                )
            ),
        )

    def _add_rate(self):
        try:
            self._tc_obj.netem_param.validate_bandwidth_rate()
        except ParameterError:
            return 0

        base_command = self._tc_obj.get_tc_command(TcSubCommand.QDISC)
        parent = "{:x}:{:d}".format(
            self._get_netem_qdisc_major_id(self._tc_obj.qdisc_major_id), self._get_qdisc_minor_id()
        )
        handle = "{:d}:".format(20)
        upper_limit_rate = get_upper_limit_rate(self._tc_device)

        bandwidth = self._tc_obj.netem_param.bandwidth_rate
        if bandwidth is None:
            bandwidth = upper_limit_rate

        command = " ".join(
            [
                base_command,
                self._dev,
                "parent {:s}".format(parent),
                "handle {:s}".format(handle),
                self.algorithm_name,
                "rate {}kbit".format(bandwidth.kilo_bps),
                "buffer {:d}".format(
                    max(int(bandwidth.kilo_bps), self.__MIN_BUFFER_BYTE)
                ),  # [byte]
                "limit 10000",
            ]
        )

        run_command_helper(
            command,
            ignore_error_msg_regexp=self._tc_obj.REGEXP_FILE_EXISTS,
            notice_msg=self._tc_obj.EXISTS_MSG_TEMPLATE.format(
                "failed to '{command:s}': qdisc already exists ({dev:s}, algo={algorithm:s}, "
                "parent={parent:s}, handle={handle:s}).".format(
                    command=base_command,
                    dev=self._dev,
                    algorithm=self.algorithm_name,
                    parent=parent,
                    handle=handle,
                )
            ),
        )

        self.__set_pre_network_filter()

    def set_shaping(self):
        with logging_context("_make_qdisc"):
            self._make_qdisc()

        with logging_context("_set_netem"):
            self._set_netem()

        with logging_context("_add_rate"):
            self._add_rate()

        with logging_context("_add_filter"):
            self._add_filter()

        return 0

    def __set_pre_network_filter(self):
        if self._is_use_iptables():
            return 0

        if all(
            [
                typepy.is_null_string(self._tc_obj.dst_network),
                not typepy.Integer(self._tc_obj.dst_port).is_type(),
            ]
        ):
            flowid = "{:s}:{:d}".format(self._tc_obj.qdisc_major_id_str, self._get_qdisc_minor_id())
        else:
            flowid = "{:s}:2".format(self._tc_obj.qdisc_major_id_str)

        return SubprocessRunner(
            " ".join(
                [
                    self._tc_obj.get_tc_command(TcSubCommand.FILTER),
                    self._dev,
                    "protocol {:s}".format(self._tc_obj.protocol),
                    "parent {:s}:".format(self._tc_obj.qdisc_major_id_str),
                    "prio 2 u32 match {:s} {:s} {:s}".format(
                        self._tc_obj.protocol,
                        self._get_network_direction_str(),
                        get_anywhere_network(self._tc_obj.ip_version),
                    ),
                    "flowid {:s}".format(flowid),
                ]
            )
        ).run()
tcconfig/_network.py
def sanitize_network(network, ip_version):
    """
    :return: Network string
    :rtype: str
    :raises ValueError: if the network string is invalid.
    """

    import ipaddress

    if typepy.is_null_string(network) or network.casefold() == "anywhere":
        return get_anywhere_network(ip_version)

    try:
        if ip_version == 4:
            ipaddress.IPv4Address(network)
            return network + "/32"

        if ip_version == 6:
            return ipaddress.IPv6Address(network).compressed
    except ipaddress.AddressValueError:
        pass

    # validate network str ---

    if ip_version == 4:
        return ipaddress.IPv4Network(str(network)).compressed

    if ip_version == 6:
        return ipaddress.IPv6Network(str(network)).compressed

    raise ValueError("unexpected ip version: {}".format(ip_version))
tcconfig/_common.py
def is_execute_tc_command(tc_command_output):
    return tc_command_output == TcCommandOutput.NOT_SET
tcconfig/_const.py
class ShapingAlgorithm:
    HTB = "htb"
    TBF = "tbf"
    LIST = [HTB, TBF]
tcconfig/_network.py
def verify_network_interface(device, tc_command_output):
    from ._common import is_execute_tc_command

    if not is_execute_tc_command(tc_command_output):
        return

    with IPRoute() as ipr:
        avail_interfaces = [link.get_attr("IFLA_IFNAME") for link in ipr.get_links()]

    if device not in avail_interfaces:
        raise NetworkInterfaceNotFoundError(target=device)
tcconfig/_error.py
class NetworkInterfaceNotFoundError(TargetNotFoundError):
    """
    Exception raised when network interface not found.
    """

    @property
    def _target_type(self):
        return "network interface"

    def __str__(self, *args, **kwargs):
        item_list = [super().__str__(*args, **kwargs)]

        with IPRoute() as ipr:
            avail_interfaces = [link.get_attr("IFLA_IFNAME") for link in ipr.get_links()]

        item_list.append("(available interfaces: {})".format(", ".join(avail_interfaces)))

        return " ".join(item_list).strip()
tcconfig/shaper/htb.py
class HtbShaper(AbstractShaper):
    __DEFAULT_CLASS_MINOR_ID = 1

    class MinQdiscMinorId:
        OUTGOING = 40
        INCOMING = 20

    @property
    def algorithm_name(self):
        return ShapingAlgorithm.HTB

    def __init__(self, tc_obj):
        super().__init__(tc_obj)

        self.__qdisc_minor_id = None
        self.__qdisc_minor_id_count = 0
        self.__netem_major_id = None
        self.__classid_wo_shaping = None

    def _get_qdisc_minor_id(self):
        if self.__qdisc_minor_id is None:
            self.__qdisc_minor_id = self.__get_unique_qdisc_minor_id()
            logger.debug("__get_unique_qdisc_minor_id: {:d}".format(self.__qdisc_minor_id))

        return self.__qdisc_minor_id

    def _get_netem_qdisc_major_id(self, base_id):
        if self.__netem_major_id is None:
            self.__netem_major_id = self.__get_unique_netem_major_id()

        return self.__netem_major_id

    def _make_qdisc(self):
        if self._tc_obj.is_change_shaping_rule:
            return 0

        base_command = self._tc_obj.get_tc_command(TcSubCommand.QDISC)
        handle = "{:s}:".format(self._tc_obj.qdisc_major_id_str)

        if self._tc_obj.is_add_shaping_rule:
            message = None
        else:
            message = self._tc_obj.EXISTS_MSG_TEMPLATE.format(
                "failed to '{command:s}': qdisc already exists "
                "({dev:s}, handle={handle:s}, algo={algorithm:s}).".format(
                    command=base_command,
                    dev=self._dev,
                    handle=handle,
                    algorithm=self.algorithm_name,
                )
            )

        run_command_helper(
            " ".join(
                [
                    base_command,
                    self._dev,
                    "root",
                    "handle {:s}".format(handle),
                    self.algorithm_name,
                    "default {:d}".format(self.__DEFAULT_CLASS_MINOR_ID),
                ]
            ),
            ignore_error_msg_regexp=self._tc_obj.REGEXP_FILE_EXISTS,
            notice_msg=message,
            exception_class=TcAlreadyExist,
        )

        return self.__add_default_class()

    def _add_rate(self):
        base_command = self._tc_obj.get_tc_command(TcSubCommand.CLASS)
        parent = "{:s}:".format(
            self._get_tc_parent("{:s}:".format(self._tc_obj.qdisc_major_id_str)).split(":")[0]
        )
        classid = self._get_tc_parent(
            "{:s}:{:d}".format(self._tc_obj.qdisc_major_id_str, self._get_qdisc_minor_id())
        )
        upper_limit_rate = get_upper_limit_rate(self._tc_device)

        bandwidth = self._tc_obj.netem_param.bandwidth_rate
        if bandwidth is None:
            bandwidth = upper_limit_rate

        command_item_list = [
            base_command,
            self._dev,
            "parent {:s}".format(parent),
            "classid {:s}".format(classid),
            self.algorithm_name,
            "rate {}Kbit".format(bandwidth.kilo_bps),
            "ceil {}Kbit".format(bandwidth.kilo_bps),
        ]

        if bandwidth != upper_limit_rate:
            command_item_list.extend(
                [
                    "burst {}KB".format(bandwidth.kilo_byte_per_sec),
                    "cburst {}KB".format(bandwidth.kilo_byte_per_sec),
                ]
            )

        run_command_helper(
            " ".join(command_item_list),
            ignore_error_msg_regexp=self._tc_obj.REGEXP_FILE_EXISTS,
            notice_msg=self._tc_obj.EXISTS_MSG_TEMPLATE.format(
                "failed to '{command:s}': class already exists "
                "({dev:s}, parent={parent:s}, classid={classid:s}, "
                "algo={algorithm:s}).".format(
                    command=base_command,
                    dev=self._dev,
                    parent=parent,
                    classid=classid,
                    algorithm=self.algorithm_name,
                )
            ),
            exception_class=TcAlreadyExist,
        )

    def _add_exclude_filter(self):
        import subprocrunner

        if all(
            [
                typepy.is_null_string(param)
                for param in (
                    self._tc_obj.exclude_dst_network,
                    self._tc_obj.exclude_src_network,
                    self._tc_obj.exclude_dst_port,
                    self._tc_obj.exclude_src_port,
                )
            ]
        ):
            logger.debug("no exclude filter found")
            return

        command_item_list = [
            self._tc_obj.get_tc_command(TcSubCommand.FILTER),
            self._dev,
            "protocol {:s}".format(self._tc_obj.protocol),
            "parent {:s}:".format(self._tc_obj.qdisc_major_id_str),
            "prio {:d}".format(self._get_filter_prio(is_exclude_filter=True)),
            "u32",
        ]

        if typepy.is_not_null_string(self._tc_obj.exclude_dst_network):
            command_item_list.append(
                "match {:s} {:s} {:s}".format(
                    self._tc_obj.protocol_match, "dst", self._tc_obj.exclude_dst_network
                )
            )

        if typepy.is_not_null_string(self._tc_obj.exclude_src_network):
            command_item_list.append(
                "match {:s} {:s} {:s}".format(
                    self._tc_obj.protocol_match, "src", self._tc_obj.exclude_src_network
                )
            )

        if typepy.is_not_null_string(self._tc_obj.exclude_dst_port):
            command_item_list.append(
                "match {:s} {:s} {:s} 0xffff".format(
                    self._tc_obj.protocol_match, "dport", self._tc_obj.exclude_dst_port
                )
            )

        if typepy.is_not_null_string(self._tc_obj.exclude_src_port):
            command_item_list.append(
                "match {:s} {:s} {:s} 0xffff".format(
                    self._tc_obj.protocol_match, "sport", self._tc_obj.exclude_src_port
                )
            )

        command_item_list.append("flowid {:s}".format(self.__classid_wo_shaping))

        return subprocrunner.SubprocessRunner(" ".join(command_item_list)).run()

    def set_shaping(self):
        is_add_shaping_rule = self._tc_obj.is_add_shaping_rule

        with logging_context("_make_qdisc"):
            try:
                self._make_qdisc()
            except TcAlreadyExist:
                if not is_add_shaping_rule:
                    return errno.EINVAL

        with logging_context("_add_rate"):
            try:
                self._add_rate()
            except TcAlreadyExist:
                if not is_add_shaping_rule:
                    return errno.EINVAL

        with logging_context("_set_netem"):
            self._set_netem()

        with logging_context("_add_exclude_filter"):
            self._add_exclude_filter()

        with logging_context("_add_filter"):
            self._add_filter()

        return 0

    def __get_unique_qdisc_minor_id(self):
        if not is_execute_tc_command(self._tc_obj.tc_command_output):
            return (
                int(self._tc_obj.netem_param.calc_hash(self._tc_obj.make_srcdst_text())[-2:], 16)
                + 1
            )

        if self._tc_obj.is_change_shaping_rule:
            self.__qdisc_minor_id_count += 1

            return self.__DEFAULT_CLASS_MINOR_ID + self.__qdisc_minor_id_count

        exist_class_item_list = re.findall(
            "class {algorithm:s} {qdisc_major_id:s}:[0-9]+".format(
                algorithm=ShapingAlgorithm.HTB, qdisc_major_id=self._tc_obj.qdisc_major_id_str
            ),
            run_tc_show(TcSubCommand.CLASS, self._tc_device, self._tc_obj.tc_command_output),
            re.MULTILINE,
        )

        exist_class_minor_id_list = []
        for class_item in exist_class_item_list:
            try:
                exist_class_minor_id_list.append(typepy.Integer(class_item.split(":")[1]).convert())
            except typepy.TypeConversionError:
                continue

        logger.debug("existing class list with {:s}: {}".format(self._dev, exist_class_item_list))
        logger.debug(
            "existing minor classid list with {:s}: {}".format(self._dev, exist_class_minor_id_list)
        )

        next_minor_id = self.__DEFAULT_CLASS_MINOR_ID
        while True:
            if next_minor_id not in exist_class_minor_id_list:
                break

            next_minor_id += 1

        return next_minor_id

    def __extract_exist_netem_major_ids(self) -> List[int]:
        exist_netem_items = re.findall(
            "qdisc [a-z]+ [a-z0-9]+",
            run_tc_show(TcSubCommand.QDISC, self._tc_device, self._tc_obj.tc_command_output),
            re.MULTILINE,
        )

        logger.debug("existing netem list with {:s}: {}".format(self._dev, exist_netem_items))

        exist_netem_major_id_list = []
        for netem_item in exist_netem_items:
            exist_netem_major_id_list.append(int(netem_item.split()[-1], 16))

        return exist_netem_major_id_list

    def __get_unique_netem_major_id(self):
        exist_netem_major_ids = self.__extract_exist_netem_major_ids()

        logger.debug(
            "existing netem major id list with {:s}: {}".format(self._dev, exist_netem_major_ids)
        )

        next_netem_major_id = self._tc_obj.netem_param.calc_device_qdisc_major_id()
        while True:
            if next_netem_major_id not in exist_netem_major_ids:
                break

            next_netem_major_id += 1

        return next_netem_major_id

    def __add_default_class(self):
        base_command = self._tc_obj.get_tc_command(TcSubCommand.CLASS)
        parent = "{:s}:".format(self._tc_obj.qdisc_major_id_str)
        classid = "{:s}:{:d}".format(self._tc_obj.qdisc_major_id_str, self.__DEFAULT_CLASS_MINOR_ID)
        self.__classid_wo_shaping = classid

        if self._tc_obj.is_add_shaping_rule:
            message = None
        else:
            message = self._tc_obj.EXISTS_MSG_TEMPLATE.format(
                "failed to default '{command:s}': class already exists "
                "({dev}, parent={parent:s}, classid={classid:s}, "
                "algo={algorithm:s})".format(
                    command=base_command,
                    dev=self._dev,
                    parent=parent,
                    classid=classid,
                    algorithm=self.algorithm_name,
                )
            )

        return run_command_helper(
            " ".join(
                [
                    base_command,
                    self._dev,
                    "parent {:s}".format(parent),
                    "classid {:s}".format(classid),
                    self.algorithm_name,
                    "rate {}kbit".format(get_upper_limit_rate(self._tc_device).kilo_bps),
                ]
            ),
            ignore_error_msg_regexp=self._tc_obj.REGEXP_FILE_EXISTS,
            notice_msg=message,
        )
tcconfig/_common.py
def run_command_helper(
    command, ignore_error_msg_regexp, notice_msg, msg_log_level="WARNING", exception_class=None
):
    runner = spr.SubprocessRunner(command, error_log_level="QUIET")
    runner.run()

    returncode = runner.returncode
    if returncode == 0:
        return 0

    if ignore_error_msg_regexp:
        if ignore_error_msg_regexp.search(runner.stderr) is None:
            error_msg = "\n".join(
                [
                    "command execution failed",
                    "  command={}".format(command),
                    "  stderr={}".format(runner.stderr),
                ]
            )

            if re.search("RTNETLINK answers: Operation not permitted", runner.stderr):
                logger.error(error_msg)
                sys.exit(returncode)

            logger.error(error_msg)

            return returncode
        else:
            # ignorable error occurred
            returncode = 0

    if typepy.is_not_null_string(notice_msg):
        logger.log(msg_log_level, notice_msg)

    if exception_class is not None:
        raise exception_class(command)

    return returncode
tcconfig/_const.py
class Tc:
    class Command:
        TCSET = "tcset"
        TCDEL = "tcdel"
        TCSHOW = "tcshow"

    class Param:
        DEVICE = "device"
        DIRECTION = "direction"
        FILTER_ID = "filter_id"
        CLASS_ID = "classid"
        DST_NETWORK = "dst-network"
        DST_PORT = "dst-port"
        FLOW_ID = "flowid"
        HANDLE = "handle"
        PARENT = "parent"
        PRIORITY = "priority"
        PROTOCOL = "protocol"
        SRC_NETWORK = "src-network"
        SRC_PORT = "src-port"

    class ValueRange:
        class LatencyTime:
            MIN = "0ms"
            MAX = "60min"

    class Min:
        LATENCY_TIME = "0ms"

    class Max:
        LATENCY_TIME = "60min"
tcconfig/_const.py
class TrafficDirection:
    OUTGOING = "outgoing"
    INCOMING = "incoming"
    LIST = [OUTGOING, INCOMING]
tcconfig/_common.py
def validate_within_min_max(param_name, value, min_value, max_value, unit):
    from dataproperty import DataProperty

    if value is None:
        return

    if unit is None:
        unit = ""
    else:
        unit = "[{:s}]".format(unit)

    if value > max_value:
        raise ParameterError(
            "'{:s}' is too high".format(param_name),
            expected="<={:s}{:s}".format(DataProperty(max_value).to_str(), unit),
            value="{:s}{:s}".format(DataProperty(value).to_str(), unit),
        )

    if value < min_value:
        raise ParameterError(
            "'{:s}' is too low".format(param_name),
            expected=">={:s}{:s}".format(DataProperty(min_value).to_str(), unit),
            value="{:s}{:s}".format(DataProperty(value).to_str(), unit),
        )
tcconfig/_const.py
class TcCommandOutput:
    NOT_SET = None
    STDOUT = "STDOUT"
    SCRIPT = "SCRIPT"
tcconfig/_shaping_rule_finder.py
class TcShapingRuleFinder:
    @property
    def _parser(self):
        self.__shaping_rule_parser.parse()

        return self.__shaping_rule_parser

    def __init__(self, logger, tc):
        self.__logger = logger
        self.__tc = tc
        self.__shaping_rule_parser = TcShapingRuleParser(
            device=self.__tc.device,
            ip_version=self.__tc.ip_version,
            tc_command_output=self.__tc.tc_command_output,
            logger=self.__logger,
        )

    def clear(self):
        self.__shaping_rule_parser.clear()

    def find_qdisc_handle(self, parent):
        for qdisc in Qdisc.select(where=Where(Tc.Param.PARENT, parent)):
            return qdisc.handle

        return None

    def find_filter_param(self):
        where_query = And(self.__get_filter_conditions())
        table_name = Filter.get_table_name()
        self.__logger.debug("find filter param: table={}, where={}".format(table_name, where_query))

        for record in Filter.select(where=where_query):
            return record.as_dict()

        self.__logger.debug(
            "find filter param: empty result (table={}, where={})".format(table_name, where_query)
        )

        return None

    def find_parent(self):
        for record in Filter.select(where=And(self.__get_filter_conditions())):
            return record.flowid

        return None

    def is_exist_rule(self):
        return self.find_parent() is not None

    def is_any_filter(self):
        return Filter.fetch_num_records() > 0

    def is_empty_filter_condition(self):
        from typepy import is_null_string

        return all(
            [
                is_anywhere_network(self.__tc.dst_network, self.__tc.ip_version),
                is_anywhere_network(self.__tc.src_network, self.__tc.ip_version),
                is_null_string(self.__tc.dst_port),
                is_null_string(self.__tc.src_port),
            ]
        )

    def get_parsed_device(self):
        if self.__tc.direction == TrafficDirection.OUTGOING:
            device = self._parser.device
        elif self.__tc.direction == TrafficDirection.INCOMING:
            device = self._parser.ifb_device

        return device

    def get_filter_string(self):
        return ", ".join(
            [where.to_query() for where in self.__get_filter_conditions() if where.value]
        )

    def __get_filter_conditions(self):
        if self.__tc.direction == TrafficDirection.OUTGOING:
            device = self._parser.device
        elif self.__tc.direction == TrafficDirection.INCOMING:
            device = self._parser.ifb_device

        return [
            Where(Tc.Param.DEVICE, device),
            Where(Tc.Param.PROTOCOL, self.__tc.protocol),
            Where(Tc.Param.DST_NETWORK, self.__tc.dst_network),
            Where(Tc.Param.SRC_NETWORK, self.__tc.src_network),
            Where(Tc.Param.DST_PORT, self.__tc.dst_port),
            Where(Tc.Param.SRC_PORT, self.__tc.src_port),
        ]
tcconfig/_iptables.py
def get_iptables_base_command():
    iptables_path = find_bin_path("iptables")

    if iptables_path:
        if re.search("iptables$", iptables_path):
            return iptables_path

        # debian/ubuntu may return /sbin/xtables-multi
        return "{:s} iptables".format(iptables_path)

    return None
tcconfig/_logger.py
MODULE_NAME = "tcconfig"
def set_logger(is_enable):
def set_log_level(log_level):
tcconfig/_iptables.py
class IptablesMangleController:

    __RE_CHAIN = re.compile("Chain {:s} |Chain {:s} |Chain {:s} ".format(*VALID_CHAIN_LIST))
    __RE_CHAIN_NAME = re.compile("{:s}|{:s}|{:s}".format(*VALID_CHAIN_LIST))
    __MAX_MARK_ID = 0xFFFFFFFF
    __MARK_ID_OFFSET = 100

    @property
    def enable(self):
        return self.__enable

    def __init__(self, enable, ip_version):
        self.__enable = enable
        self.__ip_version = ip_version

    def clear(self):
        if not self.enable:
            return

        self.__check_execution_authority()

        for mangle in self.parse():
            proc = SubprocessRunner(mangle.to_delete_command())
            if proc.run() != 0:
                raise OSError(proc.returncode, proc.stderr)

    def get_iptables(self):
        self.__check_execution_authority()

        proc = SubprocessRunner(
            "{:s} {:s}".format(get_iptables_base_command(), LIST_MANGLE_TABLE_OPTION)
        )
        if proc.run() != 0:
            raise OSError(proc.returncode, proc.stderr)

        return proc.stdout

    def get_unique_mark_id(self):
        self.__check_execution_authority()

        mark_id_list = [mangle.mark_id for mangle in self.parse()]
        logger.debug("mangle mark list: {}".format(mark_id_list))

        unique_mark_id = 1 + self.__MARK_ID_OFFSET
        while unique_mark_id < self.__MAX_MARK_ID:
            if unique_mark_id not in mark_id_list:
                return unique_mark_id

            unique_mark_id += 1

        raise RuntimeError("usable mark id not found")

    def parse(self):
        self.__check_execution_authority()

        MANGLE_ITEM_COUNT = 6

        for block in split_line_list(self.get_iptables().splitlines()):
            if len(block) <= 1:
                # skip if there is no mangle table entry exists
                continue

            match = self.__RE_CHAIN.search(block[0])
            if match is None:
                continue

            chain = self.__RE_CHAIN_NAME.search(match.group()).group()

            for line in reversed(block[2:]):
                item_list = line.split()
                if len(item_list) < MANGLE_ITEM_COUNT:
                    continue

                line_number = int(item_list[0])
                target = item_list[1]
                protocol = item_list[2]
                source = item_list[4]
                destination = item_list[5]

                try:
                    mark = int(item_list[-1], 16)
                except ValueError:
                    continue

                if target != "MARK":
                    continue

                yield IptablesMangleMarkEntry(
                    ip_version=self.__ip_version,
                    mark_id=mark,
                    source=source,
                    destination=destination,
                    chain=chain,
                    protocol=protocol,
                    line_number=line_number,
                )

    @classmethod
    def add(cls, mangling_mark):
        if not cls.enable:
            return 0

        cls.__check_execution_authority()

        return SubprocessRunner(mangling_mark.to_append_command()).run()

    @staticmethod
    def __check_execution_authority():
        from ._capabilities import get_permission_error_message, has_execution_authority

        if not has_execution_authority("iptables"):
            raise OSError(errno.EPERM, get_permission_error_message("iptables"))
tcconfig/_common.py
@contextlib.contextmanager
def logging_context(name):
    logger.debug("|---- {:s}: {:s} -----".format("start", name))
    try:
        yield
    finally:
        logger.debug("----- {:s}: {:s} ----|".format("complete", name))
tcconfig/_const.py
LIST_MANGLE_TABLE_OPTION = "-t mangle --line-numbers -L"
tcconfig/_common.py
def find_bin_path(command):
    def _to_regular_bin_path(file_path):
        path_obj = Path(file_path)
        if path_obj.islink():
            return path_obj.readlinkabs()

        return file_path

    if command in _bin_path_cache:
        return _bin_path_cache.get(command)

    bin_path = spr.Which(command, follow_symlinks=True)
    if bin_path.is_exist():
        _bin_path_cache[command] = bin_path.abspath()
        return _bin_path_cache[command]

    for sbin_path in ("/sbin/{:s}".format(command), "/usr/sbin/{:s}".format(command)):
        if os.path.isfile(sbin_path):
            _bin_path_cache[command] = _to_regular_bin_path(sbin_path)
            return _bin_path_cache[command]

    # return the command as it is when binary file not found
    return command


Question: import errno
import re
import msgfy
import subprocrunner as spr
import typepy
        import hashlib
from humanreadable import ParameterError
from ._common import (
    find_bin_path,
    is_execute_tc_command,
    logging_context,
    run_command_helper,
    validate_within_min_max,
)
from ._const import (
    LIST_MANGLE_TABLE_OPTION,
    ShapingAlgorithm,
    Tc,
    TcCommandOutput,
    TcSubCommand,
    TrafficDirection,
)
from ._error import NetworkInterfaceNotFoundError
from ._iptables import IptablesMangleController, get_iptables_base_command
from ._logger import logger
from ._network import sanitize_network, verify_network_interface
from ._shaping_rule_finder import TcShapingRuleFinder
from ._tc_command_helper import get_tc_base_command
from .shaper.htb import HtbShaper
from .shaper.tbf import TbfShaper
        from ._capabilities import get_permission_error_message, has_execution_authority

    @property
    def qdisc_major_id(self):
        return self.__qdisc_major_id

    @property
    def qdisc_major_id_str(self):
        return "{:x}".format(self.__qdisc_major_id)

    @property
    def ip_version(self):
        return 6 if self.__is_ipv6 else 4

    @property
    def protocol(self):
        return "ipv6" if self.__is_ipv6 else "ip"

    @property
    def protocol_match(self):
        return "ip6" if self.__is_ipv6 else "ip"

    @property
    def tc_command_output(self):
        return self.__tc_command_output

    @property
    def iptables_ctrl(self):
        return self.__iptables_ctrl

    def __init__(
        self,
        device,
        direction=None,
        netem_param=None,
        dst_network=None,
        exclude_dst_network=None,
        src_network=None,
        exclude_src_network=None,
        dst_port=None,
        exclude_dst_port=None,
        src_port=None,
        exclude_src_port=None,
        is_ipv6=False,
        is_change_shaping_rule=False,
        is_add_shaping_rule=False,
        is_enable_iptables=False,
        shaping_algorithm=None,
        tc_command_output=TcCommandOutput.NOT_SET,
    ):
        self.__device = device

        self.__direction = direction
        self.__netem_param = netem_param
        self.__dst_network = dst_network
        self.__exclude_dst_network = exclude_dst_network
        self.__src_network = src_network
        self.__exclude_src_network = exclude_src_network
        self.__src_port = src_port
        self.__exclude_src_port = exclude_src_port
        self.__dst_port = dst_port
        self.__exclude_dst_port = exclude_dst_port
        self.__is_ipv6 = is_ipv6
        self.__is_change_shaping_rule = is_change_shaping_rule
        self.__is_add_shaping_rule = is_add_shaping_rule
        self.__is_enable_iptables = is_enable_iptables
        self.__tc_command_output = tc_command_output

        self.__qdisc_major_id = self.__get_device_qdisc_major_id()

        self.__iptables_ctrl = IptablesMangleController(is_enable_iptables, self.ip_version)

        self.__init_shaper(shaping_algorithm)

    def validate(self):
        verify_network_interface(self.device, self.__tc_command_output)
        self.__netem_param.validate_netem_parameter()
        self.__validate_src_network()
        self.__validate_port()

    def __validate_src_network(self):
        if any(
            [
                typepy.is_null_string(self.src_network),
                self.__shaper.algorithm_name == ShapingAlgorithm.HTB,
            ]
        ):
            return

        if not self.is_enable_iptables:
            raise ParameterError(
                "--iptables option required to use --src-network option",
                value=self.is_enable_iptables,
            )

    def sanitize(self):
        self.__dst_network = sanitize_network(self.dst_network, self.ip_version)
        self.__src_network = sanitize_network(self.src_network, self.ip_version)

    def get_tc_device(self):
        """
        Return a device name that associated network communication direction.
        """

        if self.direction == TrafficDirection.OUTGOING:
            return self.device

        if self.direction == TrafficDirection.INCOMING:
            return self.ifb_device

        raise ParameterError(
            "unknown direction", expected=TrafficDirection.LIST, value=self.direction
        )

    def get_tc_command(self, subcommand):
        return "{:s} {:s}".format(
            get_tc_base_command(subcommand), "change" if self.is_change_shaping_rule else "add"
        )

    def get_command_history(self):
        def tc_command_filter(command):

Answer:             if get_iptables_base_command():