From 051e063fdf2e459a0716a35778b33ea6bb2fdcb6 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Mon, 31 Oct 2022 14:26:51 +0100
Subject: firewall: T970: Refactor domain resolver, add firewall
source/destination `fqdn` node
---
smoketest/scripts/cli/test_firewall.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
(limited to 'smoketest/scripts/cli')
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 821925bcd..e172e086d 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -17,11 +17,13 @@
import unittest
from glob import glob
+from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
+from vyos.util import run
sysfs_config = {
'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'},
@@ -76,6 +78,17 @@ class TestFirewall(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_geoip(self):
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'drop'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se'])
@@ -125,6 +138,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'interface', 'eth0', 'in', 'name', 'smoketest'])
self.cli_commit()
+
+ self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5')
+
nftables_search = [
['iifname "eth0"', 'jump NAME_smoketest'],
['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'return'],
--
cgit v1.2.3
From b4b491d424fba6f3d417135adc1865e338a480a1 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Mon, 31 Oct 2022 21:08:42 +0100
Subject: nat: T1877: T970: Add firewall groups to NAT
---
data/templates/firewall/nftables-nat.j2 | 4 ++
interface-definitions/include/nat-rule.xml.i | 2 +
python/vyos/firewall.py | 2 +
python/vyos/nat.py | 56 +++++++++++++++++++--
smoketest/scripts/cli/test_nat.py | 35 +++++++++++++
src/conf_mode/firewall.py | 22 ++++++---
src/conf_mode/nat.py | 73 +++++++++++++++++++++++-----
src/helpers/vyos-domain-resolver.py | 1 +
8 files changed, 174 insertions(+), 21 deletions(-)
(limited to 'smoketest/scripts/cli')
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 @@
#include
#include
+ #include
#include
@@ -285,6 +286,7 @@
#include
#include
+ #include
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 = {
--
cgit v1.2.3