diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/configdiff.py | 30 | ||||
| -rw-r--r-- | python/vyos/firewall.py | 217 | ||||
| -rwxr-xr-x | python/vyos/ifconfig/interface.py | 45 | ||||
| -rw-r--r-- | python/vyos/template.py | 41 | 
4 files changed, 307 insertions, 26 deletions
diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py index 0e41fbe27..4ad7443d7 100644 --- a/python/vyos/configdiff.py +++ b/python/vyos/configdiff.py @@ -17,7 +17,9 @@ from enum import IntFlag, auto  from vyos.config import Config  from vyos.configdict import dict_merge +from vyos.configdict import list_diff  from vyos.util import get_sub_dict, mangle_dict_keys +from vyos.util import dict_search_args  from vyos.xml import defaults  class ConfigDiffError(Exception): @@ -134,6 +136,34 @@ class ConfigDiff(object):                                                      self._key_mangling[1])          return config_dict +    def get_child_nodes_diff_str(self, path=[]): +        ret = {'add': {}, 'change': {}, 'delete': {}} + +        diff = self.get_child_nodes_diff(path, +                                expand_nodes=Diff.ADD | Diff.DELETE | Diff.MERGE | Diff.STABLE, +                                no_defaults=True) + +        def parse_dict(diff_dict, diff_type, prefix=[]): +            for k, v in diff_dict.items(): +                if isinstance(v, dict): +                    parse_dict(v, diff_type, prefix + [k]) +                else: +                    path_str = ' '.join(prefix + [k]) +                    if diff_type == 'add' or diff_type == 'delete': +                        if isinstance(v, list): +                            v = ', '.join(v) +                        ret[diff_type][path_str] = v +                    elif diff_type == 'merge': +                        old_value = dict_search_args(diff['stable'], *prefix, k) +                        if old_value and old_value != v: +                            ret['change'][path_str] = [old_value, v] + +        parse_dict(diff['merge'], 'merge') +        parse_dict(diff['add'], 'add') +        parse_dict(diff['delete'], 'delete') + +        return ret +      def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False):          """          Args: diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py new file mode 100644 index 000000000..8b7402b7e --- /dev/null +++ b/python/vyos/firewall.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import re + +from vyos.util import cmd +from vyos.util import dict_search_args + +def find_nftables_rule(table, chain, rule_matches=[]): +    # Find rule in table/chain that matches all criteria and return the handle +    results = cmd(f'sudo nft -a list chain {table} {chain}').split("\n") +    for line in results: +        if all(rule_match in line for rule_match in rule_matches): +            handle_search = re.search('handle (\d+)', line) +            if handle_search: +                return handle_search[1] +    return None + +def remove_nftables_rule(table, chain, handle): +    cmd(f'sudo nft delete rule {table} {chain} handle {handle}') + +# Functions below used by template generation + +def nft_action(vyos_action): +    if vyos_action == 'accept': +        return 'return' +    return vyos_action + +def parse_rule(rule_conf, fw_name, rule_id, ip_name): +    output = [] +    def_suffix = '6' if ip_name == 'ip6' else '' + +    if 'state' in rule_conf and rule_conf['state']: +        states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable']) +        output.append(f'ct state {{{states}}}') + +    if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': +        proto = rule_conf['protocol'] +        if proto == 'tcp_udp': +            proto = '{tcp, udp}' +        output.append('meta l4proto ' + proto) + +    for side in ['destination', 'source']: +        if side in rule_conf: +            prefix = side[0] +            side_conf = rule_conf[side] + +            if 'address' in side_conf: +                output.append(f'{ip_name} {prefix}addr {side_conf["address"]}') + +            if 'mac_address' in side_conf: +                suffix = side_conf["mac_address"] +                if suffix[0] == '!': +                    suffix = f'!= {suffix[1:]}' +                output.append(f'ether {prefix}addr {suffix}') + +            if 'port' in side_conf: +                proto = rule_conf['protocol'] +                port = side_conf["port"] + +                if isinstance(port, list): +                    port = ",".join(port) + +                if proto == 'tcp_udp': +                    proto = 'th' + +                output.append(f'{proto} {prefix}port {{{port}}}') + +            if 'group' in side_conf: +                group = side_conf['group'] +                if 'address_group' in group: +                    group_name = group['address_group'] +                    output.append(f'{ip_name} {prefix}addr $A{def_suffix}_{group_name}') +                elif 'network_group' in group: +                    group_name = group['network_group'] +                    output.append(f'{ip_name} {prefix}addr $N{def_suffix}_{group_name}') +                if 'port_group' in group: +                    proto = rule_conf['protocol'] +                    group_name = group['port_group'] + +                    if proto == 'tcp_udp': +                        proto = 'th' + +                    output.append(f'{proto} {prefix}port $P_{group_name}') + +    if 'log' in rule_conf and rule_conf['log'] == 'enable': +        output.append('log') + +    if 'hop_limit' in rule_conf: +        operators = {'eq': '==', 'gt': '>', 'lt': '<'} +        for op, operator in operators.items(): +            if op in rule_conf['hop_limit']: +                value = rule_conf['hop_limit'][op] +                output.append(f'ip6 hoplimit {operator} {value}') + +    for icmp in ['icmp', 'icmpv6']: +        if icmp in rule_conf: +            if 'type_name' in rule_conf[icmp]: +                output.append(icmp + ' type ' + rule_conf[icmp]['type_name']) +            else: +                if 'code' in rule_conf[icmp]: +                    output.append(icmp + ' code ' + rule_conf[icmp]['code']) +                if 'type' in rule_conf[icmp]: +                    output.append(icmp + ' type ' + rule_conf[icmp]['type']) + +    if 'ipsec' in rule_conf: +        if 'match_ipsec' in rule_conf['ipsec']: +            output.append('meta ipsec == 1') +        if 'match_non_ipsec' in rule_conf['ipsec']: +            output.append('meta ipsec == 0') + +    if 'fragment' in rule_conf: +        # Checking for fragmentation after priority -400 is not possible, +        # so we use a priority -450 hook to set a mark +        if 'match_frag' in rule_conf['fragment']: +            output.append('meta mark 0xffff1') +        if 'match_non_frag' in rule_conf['fragment']: +            output.append('meta mark != 0xffff1') + +    if 'limit' in rule_conf: +        if 'rate' in rule_conf['limit']: +            output.append(f'limit rate {rule_conf["limit"]["rate"]}/second') +            if 'burst' in rule_conf['limit']: +                output.append(f'burst {rule_conf["limit"]["burst"]} packets') + +    if 'recent' in rule_conf: +        count = rule_conf['recent']['count'] +        time = rule_conf['recent']['time'] +        # output.append(f'meter {fw_name}_{rule_id} {{ ip saddr and 255.255.255.255 limit rate over {count}/{time} burst {count} packets }}') +        # Waiting on input from nftables developers due to +        # bug with above line and atomic chain flushing. + +    if 'time' in rule_conf: +        output.append(parse_time(rule_conf['time'])) + +    tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') +    if tcp_flags: +        output.append(parse_tcp_flags(tcp_flags)) + + +    output.append('counter') + +    if 'set' in rule_conf: +        output.append(parse_policy_set(rule_conf['set'], def_suffix)) + +    if 'action' in rule_conf: +        output.append(nft_action(rule_conf['action'])) +    else: +        output.append('return') + +    output.append(f'comment "{fw_name}-{rule_id}"') +    return " ".join(output) + +def parse_tcp_flags(flags): +    all_flags = [] +    include = [] +    for flag in flags.split(","): +        if flag[0] == '!': +            flag = flag[1:] +        else: +            include.append(flag) +        all_flags.append(flag) +    return f'tcp flags & ({"|".join(all_flags)}) == {"|".join(include)}' + +def parse_time(time): +    out = [] +    if 'startdate' in time: +        start = time['startdate'] +        if 'T' not in start and 'starttime' in time: +            start += f' {time["starttime"]}' +        out.append(f'time >= "{start}"') +    if 'starttime' in time and 'startdate' not in time: +        out.append(f'hour >= "{time["starttime"]}"') +    if 'stopdate' in time: +        stop = time['stopdate'] +        if 'T' not in stop and 'stoptime' in time: +            stop += f' {time["stoptime"]}' +        out.append(f'time < "{stop}"') +    if 'stoptime' in time and 'stopdate' not in time: +        out.append(f'hour < "{time["stoptime"]}"') +    if 'weekdays' in time: +        days = time['weekdays'].split(",") +        out_days = [f'"{day}"' for day in days if day[0] != '!'] +        out.append(f'day {{{",".join(out_days)}}}') +    return " ".join(out) + +def parse_policy_set(set_conf, def_suffix): +    out = [] +    if 'dscp' in set_conf: +        dscp = set_conf['dscp'] +        out.append(f'ip{def_suffix} dscp set {dscp}') +    if 'mark' in set_conf: +        mark = set_conf['mark'] +        out.append(f'meta mark set {mark}') +    if 'table' in set_conf: +        table = set_conf['table'] +        if table == 'main': +            table = '254' +        mark = 0x7FFFFFFF - int(set_conf['table']) +        out.append(f'meta mark set {mark}') +    if 'tcp_mss' in set_conf: +        mss = set_conf['tcp_mss'] +        out.append(f'tcp option maxseg size set {mss}') +    return " ".join(out) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 5fdd27828..91c7f0c33 100755 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -577,6 +577,15 @@ class Interface(Control):              return None          return self.set_interface('arp_cache_tmo', tmo) +    def _cleanup_mss_rules(self, table, ifname): +        commands = [] +        results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n") +        for line in results: +            if f'oifname "{ifname}"' in line: +                handle_search = re.search('handle (\d+)', line) +                if handle_search: +                    self._cmd(f'nft delete rule {table} VYOS_TCP_MSS handle {handle_search[1]}') +      def set_tcp_ipv4_mss(self, mss):          """          Set IPv4 TCP MSS value advertised when TCP SYN packets leave this @@ -588,22 +597,14 @@ class Interface(Control):          >>> from vyos.ifconfig import Interface          >>> Interface('eth0').set_tcp_ipv4_mss(1340)          """ -        iptables_bin = 'iptables' -        base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN' -        out = self._cmd(f'{iptables_bin}-save -t mangle') -        for line in out.splitlines(): -            if line.startswith(base_options): -                # remove OLD MSS mangling configuration -                line = line.replace('-A FORWARD', '-D FORWARD') -                self._cmd(f'{iptables_bin} -t mangle {line}') - -        cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS' +        self._cleanup_mss_rules('raw', self.ifname) +        nft_prefix = 'nft add rule raw VYOS_TCP_MSS' +        base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn'          if mss == 'clamp-mss-to-pmtu': -            self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu') +            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'")          elif int(mss) > 0: -            # probably add option to clamp only if bigger:              low_mss = str(int(mss) + 1) -            self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}') +            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'")      def set_tcp_ipv6_mss(self, mss):          """ @@ -616,22 +617,14 @@ class Interface(Control):          >>> from vyos.ifconfig import Interface          >>> Interface('eth0').set_tcp_mss(1320)          """ -        iptables_bin = 'ip6tables' -        base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN' -        out = self._cmd(f'{iptables_bin}-save -t mangle') -        for line in out.splitlines(): -            if line.startswith(base_options): -                # remove OLD MSS mangling configuration -                line = line.replace('-A FORWARD', '-D FORWARD') -                self._cmd(f'{iptables_bin} -t mangle {line}') - -        cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS' +        self._cleanup_mss_rules('ip6 raw', self.ifname) +        nft_prefix = 'nft add rule ip6 raw VYOS_TCP_MSS' +        base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn'          if mss == 'clamp-mss-to-pmtu': -            self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu') +            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'")          elif int(mss) > 0: -            # probably add option to clamp only if bigger:              low_mss = str(int(mss) + 1) -            self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}') +            self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'")      def set_arp_filter(self, arp_filter):          """ diff --git a/python/vyos/template.py b/python/vyos/template.py index f694b53e0..2987fcd0e 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -22,6 +22,7 @@ from jinja2 import FileSystemLoader  from vyos.defaults import directories  from vyos.util import chmod  from vyos.util import chown +from vyos.util import dict_search_args  from vyos.util import makedir  # Holds template filters registered via register_filter() @@ -503,3 +504,43 @@ def snmp_auth_oid(type):          'none': '.1.3.6.1.6.3.10.1.2.1'      }      return OIDs[type] + +@register_filter('nft_action') +def nft_action(vyos_action): +    if vyos_action == 'accept': +        return 'return' +    return vyos_action + +@register_filter('nft_rule') +def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'): +    from vyos.firewall import parse_rule +    return parse_rule(rule_conf, fw_name, rule_id, ip_name) + +@register_filter('nft_state_policy') +def nft_state_policy(conf, state): +    out = [f'ct state {state}'] + +    if 'log' in conf and 'enable' in conf['log']: +        out.append('log') + +    out.append('counter') + +    if 'action' in conf: +        out.append(conf['action']) + +    return " ".join(out) + +@register_filter('nft_intra_zone_action') +def nft_intra_zone_action(zone_conf, ipv6=False): +    if 'intra_zone_filtering' in zone_conf: +        intra_zone = zone_conf['intra_zone_filtering'] +        fw_name = 'ipv6_name' if ipv6 else 'name' + +        if 'action' in intra_zone: +            if intra_zone['action'] == 'accept': +                return 'return' +            return intra_zone['action'] +        elif dict_search_args(intra_zone, 'firewall', fw_name): +            name = dict_search_args(intra_zone, 'firewall', fw_name) +            return f'jump {name}' +    return 'return'  | 
