diff options
| author | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2022-10-31 21:08:42 +0100 | 
|---|---|---|
| committer | sarthurdev <965089+sarthurdev@users.noreply.github.com> | 2022-11-03 21:09:28 +0100 | 
| commit | b4b491d424fba6f3d417135adc1865e338a480a1 (patch) | |
| tree | f6aff71905c007837110d634e6cc5d6580f6db23 | |
| parent | 051e063fdf2e459a0716a35778b33ea6bb2fdcb6 (diff) | |
| download | vyos-1x-b4b491d424fba6f3d417135adc1865e338a480a1.tar.gz vyos-1x-b4b491d424fba6f3d417135adc1865e338a480a1.zip | |
nat: T1877: T970: Add firewall groups to NAT
| -rw-r--r-- | data/templates/firewall/nftables-nat.j2 | 4 | ||||
| -rw-r--r-- | interface-definitions/include/nat-rule.xml.i | 2 | ||||
| -rw-r--r-- | python/vyos/firewall.py | 2 | ||||
| -rw-r--r-- | python/vyos/nat.py | 56 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_nat.py | 35 | ||||
| -rwxr-xr-x | src/conf_mode/firewall.py | 22 | ||||
| -rwxr-xr-x | src/conf_mode/nat.py | 73 | ||||
| -rwxr-xr-x | src/helpers/vyos-domain-resolver.py | 1 | 
8 files changed, 174 insertions, 21 deletions
| diff --git a/data/templates/firewall/nftables-nat.j2 b/data/templates/firewall/nftables-nat.j2 index c5c0a2c86..f0be3cf5d 100644 --- a/data/templates/firewall/nftables-nat.j2 +++ b/data/templates/firewall/nftables-nat.j2 @@ -1,5 +1,7 @@  #!/usr/sbin/nft -f +{% import 'firewall/nftables-defines.j2' as group_tmpl %} +  {% if helper_functions is vyos_defined('remove') %}  {# NAT if going to be disabled - remove rules and targets from nftables #}  {%     set base_command = 'delete rule ip raw' %} @@ -59,5 +61,7 @@ table ip vyos_nat {      chain VYOS_PRE_SNAT_HOOK {          return      } + +{{ group_tmpl.groups(firewall_group, False) }}  }  {% endif %} diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index 84941aa6a..8f2029388 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -20,6 +20,7 @@        <children>          #include <include/nat-address.xml.i>          #include <include/nat-port.xml.i> +        #include <include/firewall/source-destination-group.xml.i>        </children>      </node>      #include <include/generic-disable-node.xml.i> @@ -285,6 +286,7 @@        <children>          #include <include/nat-address.xml.i>          #include <include/nat-port.xml.i> +        #include <include/firewall/source-destination-group.xml.i>        </children>      </node>    </children> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index db4878c9d..59ec4948f 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -34,6 +34,8 @@ from vyos.util import dict_search_args  from vyos.util import dict_search_recursive  from vyos.util import run +# Domain Resolver +  def fqdn_config_parse(firewall):      firewall['ip_fqdn'] = {}      firewall['ip6_fqdn'] = {} diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 31bbdc386..3d01829a7 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -85,8 +85,13 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):              translation_str += f' {",".join(options)}'      for target in ['source', 'destination']: +        if target not in rule_conf: +            continue + +        side_conf = rule_conf[target]          prefix = target[:1] -        addr = dict_search_args(rule_conf, target, 'address') + +        addr = dict_search_args(side_conf, 'address')          if addr and not (ignore_type_addr and target == nat_type):              operator = ''              if addr[:1] == '!': @@ -94,7 +99,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):                  addr = addr[1:]              output.append(f'{ip_prefix} {prefix}addr {operator} {addr}') -        addr_prefix = dict_search_args(rule_conf, target, 'prefix') +        addr_prefix = dict_search_args(side_conf, 'prefix')          if addr_prefix and ipv6:              operator = ''              if addr_prefix[:1] == '!': @@ -102,7 +107,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):                  addr_prefix = addr[1:]              output.append(f'ip6 {prefix}addr {operator} {addr_prefix}') -        port = dict_search_args(rule_conf, target, 'port') +        port = dict_search_args(side_conf, 'port')          if port:              protocol = rule_conf['protocol']              if protocol == 'tcp_udp': @@ -113,6 +118,51 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):                  port = port[1:]              output.append(f'{protocol} {prefix}port {operator} {{ {port} }}') +        if 'group' in side_conf: +            group = side_conf['group'] +            if 'address_group' in group and not (ignore_type_addr and target == nat_type): +                group_name = group['address_group'] +                operator = '' +                if group_name[0] == '!': +                    operator = '!=' +                    group_name = group_name[1:] +                output.append(f'{ip_prefix} {prefix}addr {operator} @A_{group_name}') +            # Generate firewall group domain-group +            elif 'domain_group' in group and not (ignore_type_addr and target == nat_type): +                group_name = group['domain_group'] +                operator = '' +                if group_name[0] == '!': +                    operator = '!=' +                    group_name = group_name[1:] +                output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}') +            elif 'network_group' in group and not (ignore_type_addr and target == nat_type): +                group_name = group['network_group'] +                operator = '' +                if group_name[0] == '!': +                    operator = '!=' +                    group_name = group_name[1:] +                output.append(f'{ip_prefix} {prefix}addr {operator} @N_{group_name}') +            if 'mac_group' in group: +                group_name = group['mac_group'] +                operator = '' +                if group_name[0] == '!': +                    operator = '!=' +                    group_name = group_name[1:] +                output.append(f'ether {prefix}addr {operator} @M_{group_name}') +            if 'port_group' in group: +                proto = rule_conf['protocol'] +                group_name = group['port_group'] + +                if proto == 'tcp_udp': +                    proto = 'th' + +                operator = '' +                if group_name[0] == '!': +                    operator = '!=' +                    group_name = group_name[1:] + +                output.append(f'{proto} {prefix}port {operator} @P_{group_name}') +      output.append('counter')      if 'log' in rule_conf: diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 2ae90fcaf..9f4e3b831 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -58,6 +58,17 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):                      break              self.assertTrue(not matched if inverse else matched, msg=search) +    def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): +        # Resolver no longer blocks commit, need to wait for daemon to populate set +        count = 0 +        while count < max_wait: +            code = run(f'sudo nft get element {table} {set_name} {{ {element} }}') +            if code == 0: +                return True +            count += 1 +            sleep(1) +        return False +      def test_snat(self):          rules = ['100', '110', '120', '130', '200', '210', '220', '230']          outbound_iface_100 = 'eth0' @@ -84,6 +95,30 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):          self.verify_nftables(nftables_search, 'ip vyos_nat') +    def test_snat_groups(self): +        address_group = 'smoketest_addr' +        address_group_member = '192.0.2.1' +        rule = '100' +        outbound_iface = 'eth0' + +        self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) + +        self.cli_set(src_path + ['rule', rule, 'source', 'group', 'address-group', address_group]) +        self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface]) +        self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade']) + +        self.cli_commit() + +        nftables_search = [ +            [f'set A_{address_group}'], +            [f'elements = {{ {address_group_member} }}'], +            [f'ip saddr @A_{address_group}', f'oifname "{outbound_iface}"', 'masquerade'] +        ] + +        self.verify_nftables(nftables_search, 'ip vyos_nat') + +        self.cli_delete(['firewall']) +      def test_dnat(self):          rules = ['100', '110', '120', '130', '200', '210', '220', '230']          inbound_iface_100 = 'eth0' diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 2bb765e65..783adec46 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -41,6 +41,7 @@ from vyos import ConfigError  from vyos import airbag  airbag.enable() +nat_conf_script = '/usr/libexec/vyos/conf_mode/nat.py'  policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py'  nftables_conf = '/run/nftables.conf' @@ -158,7 +159,7 @@ def get_config(config=None):          for zone in firewall['zone']:              firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) -    firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) +    firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))      if 'config_trap' in firewall and firewall['config_trap'] == 'enable':          diff = get_config_diff(conf) @@ -463,6 +464,12 @@ def post_apply_trap(firewall):                  cmd(base_cmd + ' '.join(objects)) +def resync_nat(): +    # Update nat as firewall groups were updated +    tmp, out = rc_cmd(nat_conf_script) +    if tmp > 0: +        Warning(f'Failed to re-apply nat configuration! {out}') +  def resync_policy_route():      # Update policy route as firewall groups were updated      tmp, out = rc_cmd(policy_route_conf_script) @@ -474,19 +481,20 @@ def apply(firewall):      if install_result == 1:          raise ConfigError(f'Failed to apply firewall: {output}') +    apply_sysfs(firewall) + +    if firewall['group_resync']: +        resync_nat() +        resync_policy_route() +      # T970 Enable a resolver (systemd daemon) that checks -    # domain-group addresses and update entries for domains by timeout +    # domain-group/fqdn addresses and update entries for domains by timeout      # If router loaded without internet connection or for synchronization      domain_action = 'stop'      if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']:          domain_action = 'restart'      call(f'systemctl {domain_action} vyos-domain-resolver.service') -    apply_sysfs(firewall) - -    if firewall['policy_resync']: -        resync_policy_route() -      if firewall['geoip_updated']:          # Call helper script to Update set contents          if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 978c043e9..9f8221514 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -32,6 +32,7 @@ from vyos.util import cmd  from vyos.util import run  from vyos.util import check_kmod  from vyos.util import dict_search +from vyos.util import dict_search_args  from vyos.validate import is_addr_assigned  from vyos.xml import defaults  from vyos import ConfigError @@ -47,6 +48,13 @@ else:  nftables_nat_config = '/run/nftables_nat.conf'  nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft' +valid_groups = [ +    'address_group', +    'domain_group', +    'network_group', +    'port_group' +] +  def get_handler(json, chain, target):      """ Get nftable rule handler number of given chain/target combination.      Handler is required when adding NAT/Conntrack helper targets """ @@ -60,7 +68,7 @@ def get_handler(json, chain, target):      return None -def verify_rule(config, err_msg): +def verify_rule(config, err_msg, groups_dict):      """ Common verify steps used for both source and destination NAT """      if (dict_search('translation.port', config) != None or @@ -78,6 +86,45 @@ def verify_rule(config, err_msg):                               'statically maps a whole network of addresses onto another\n' \                               'network of addresses') +    for side in ['destination', 'source']: +        if side in config: +            side_conf = config[side] + +            if len({'address', 'fqdn'} & set(side_conf)) > 1: +                raise ConfigError('Only one of address, fqdn or geoip can be specified') + +            if 'group' in side_conf: +                if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: +                    raise ConfigError('Only one address-group, network-group or domain-group can be specified') + +                for group in valid_groups: +                    if group in side_conf['group']: +                        group_name = side_conf['group'][group] +                        error_group = group.replace("_", "-") + +                        if group in ['address_group', 'network_group', 'domain_group']: +                            types = [t for t in ['address', 'fqdn'] if t in side_conf] +                            if types: +                                raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') + +                        if group_name and group_name[0] == '!': +                            group_name = group_name[1:] + +                        group_obj = dict_search_args(groups_dict, group, group_name) + +                        if group_obj is None: +                            raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') + +                        if not group_obj: +                            Warning(f'{error_group} "{group_name}" has no members!') + +            if dict_search_args(side_conf, 'group', 'port_group'): +                if 'protocol' not in config: +                    raise ConfigError('Protocol must be defined if specifying a port-group') + +                if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']: +                    raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port-group') +  def get_config(config=None):      if config:          conf = config @@ -105,16 +152,20 @@ def get_config(config=None):      condensed_json = jmespath.search(pattern, nftable_json)      if not conf.exists(base): -        nat['helper_functions'] = 'remove' - -        # Retrieve current table handler positions -        nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') -        nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') -        nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') -        nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') +        if get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER'): +            nat['helper_functions'] = 'remove' + +            # Retrieve current table handler positions +            nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') +            nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') +            nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') +            nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')          nat['deleted'] = ''          return nat +    nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, +                                    no_tag_node_value_mangle=True) +      # check if NAT connection tracking helpers need to be set up - this has to      # be done only once      if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): @@ -157,7 +208,7 @@ def verify(nat):                          Warning(f'IP address {ip} does not exist on the system!')              # common rule verification -            verify_rule(config, err_msg) +            verify_rule(config, err_msg, nat['firewall_group'])      if dict_search('destination.rule', nat): @@ -175,7 +226,7 @@ def verify(nat):                      raise ConfigError(f'{err_msg} translation requires address and/or port')              # common rule verification -            verify_rule(config, err_msg) +            verify_rule(config, err_msg, nat['firewall_group'])      if dict_search('static.rule', nat):          for rule, config in dict_search('static.rule', nat).items(): @@ -186,7 +237,7 @@ def verify(nat):                                    'inbound-interface not specified')              # common rule verification -            verify_rule(config, err_msg) +            verify_rule(config, err_msg, nat['firewall_group'])      return None diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py index 2f71f15db..035c208b2 100755 --- a/src/helpers/vyos-domain-resolver.py +++ b/src/helpers/vyos-domain-resolver.py @@ -37,6 +37,7 @@ domain_state = {}  ipv4_tables = {      'ip mangle',      'ip vyos_filter', +    'ip vyos_nat'  }  ipv6_tables = { | 
