summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/firewall/nftables.j28
-rw-r--r--interface-definitions/firewall.xml.in25
-rw-r--r--interface-definitions/include/firewall/source-destination-group.xml.i8
-rw-r--r--python/vyos/firewall.py71
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py21
-rwxr-xr-xsrc/conf_mode/firewall.py28
-rwxr-xr-xsrc/helpers/vyos-domain-group-resolve.py60
-rw-r--r--src/systemd/vyos-domain-group-resolve.service11
8 files changed, 228 insertions, 4 deletions
diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2
index fac3fad03..1f88ae40c 100644
--- a/data/templates/firewall/nftables.j2
+++ b/data/templates/firewall/nftables.j2
@@ -45,6 +45,14 @@ table ip filter {
{{ conf | nft_default_rule(name_text) }}
}
{% endfor %}
+{% if group is vyos_defined and group.domain_group is vyos_defined %}
+{% for name, name_config in group.domain_group.items() %}
+ set {{ name }} {
+ type ipv4_addr
+ flags interval
+ }
+{% endfor %}
+{% endif %}
{% for set_name in ns.sets %}
set RECENT_{{ set_name }} {
type ipv4_addr
diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index ff8d92a24..d0218579c 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -100,6 +100,31 @@
#include <include/generic-description.xml.i>
</children>
</tagNode>
+ <tagNode name="domain-group">
+ <properties>
+ <help>Firewall domain-group</help>
+ <constraint>
+ <regex>[a-zA-Z_][a-zA-Z0-9][\w\-\.]*</regex>
+ </constraint>
+ <constraintErrorMessage>Name of domain-group can only contain alpha-numeric letters, hyphen, underscores and not start with numeric</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>Domain-group member</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Domain address to match</description>
+ </valueHelp>
+ <constraint>
+ <regex>[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}?(\/.*)?</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ #include <include/generic-description.xml.i>
+ </children>
+ </tagNode>
<tagNode name="ipv6-address-group">
<properties>
<help>Firewall ipv6-address-group</help>
diff --git a/interface-definitions/include/firewall/source-destination-group.xml.i b/interface-definitions/include/firewall/source-destination-group.xml.i
index ab11e89e9..6ebee356c 100644
--- a/interface-definitions/include/firewall/source-destination-group.xml.i
+++ b/interface-definitions/include/firewall/source-destination-group.xml.i
@@ -12,6 +12,14 @@
</completionHelp>
</properties>
</leafNode>
+ <leafNode name="domain-group">
+ <properties>
+ <help>Group of domains</help>
+ <completionHelp>
+ <path>firewall group domain-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
#include <include/firewall/mac-group.xml.i>
<leafNode name="network-group">
<properties>
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 04fd44173..b962c4f18 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 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
@@ -16,9 +16,70 @@
import re
+from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search_args
+
+# Functions for firewall group domain-groups
+def get_ips_domains_dict(list_domains):
+ """
+ Get list of IPv4 addresses by list of domains
+ Ex: get_ips_domains_dict(['ex1.com', 'ex2.com'])
+ {'ex1.com': ['192.0.2.1'], 'ex2.com': ['192.0.2.2', '192.0.2.3']}
+ """
+ from socket import gethostbyname_ex
+ from socket import gaierror
+
+ ip_dict = {}
+ for domain in list_domains:
+ try:
+ _, _, ips = gethostbyname_ex(domain)
+ ip_dict[domain] = ips
+ except gaierror:
+ pass
+
+ return ip_dict
+
+def nft_init_set(group_name, table="filter", family="ip"):
+ """
+ table ip filter {
+ set GROUP_NAME
+ type ipv4_addr
+ flags interval
+ }
+ """
+ return call(f'nft add set ip {table} {group_name} {{ type ipv4_addr\\; flags interval\\; }}')
+
+
+def nft_add_set_elements(group_name, elements, table="filter", family="ip"):
+ """
+ table ip filter {
+ set GROUP_NAME {
+ type ipv4_addr
+ flags interval
+ elements = { 192.0.2.1, 192.0.2.2 }
+ }
+ """
+ elements = ", ".join(elements)
+ return call(f'nft add element {family} {table} {group_name} {{ {elements} }} ')
+
+def nft_flush_set(group_name, table="filter", family="ip"):
+ """
+ Flush elements of nft set
+ """
+ return call(f'nft flush set {family} {table} {group_name}')
+
+def nft_update_set_elements(group_name, elements, table="filter", family="ip"):
+ """
+ Update elements of nft set
+ """
+ flush_set = nft_flush_set(group_name, table="filter", family="ip")
+ nft_add_set = nft_add_set_elements(group_name, elements, table="filter", family="ip")
+ return flush_set, nft_add_set
+
+# END firewall group domain-group (sets)
+
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")
@@ -118,6 +179,14 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} $A{def_suffix}_{group_name}')
+ # Generate firewall group domain-group
+ elif 'domain_group' in group:
+ group_name = group['domain_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_name} {prefix}addr {operator} @{group_name}')
elif 'network_group' in group:
group_name = group['network_group']
operator = ''
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index b8f944575..79d22e361 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -57,10 +57,19 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
def test_groups(self):
+ hostmap_path = ['system', 'static-host-mapping', 'host-name']
+ example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11']
+
+ self.cli_set(hostmap_path + ['example.com', 'inet', '192.0.2.5'])
+ for ips in example_org:
+ self.cli_set(hostmap_path + ['example.org', 'inet', ips])
+
self.cli_set(['firewall', 'group', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05'])
self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24'])
self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53'])
self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123'])
+ self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com'])
+ self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
@@ -68,15 +77,20 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'accept'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac'])
+ self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'action', 'accept'])
+ self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain'])
self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest'])
self.cli_commit()
-
nftables_search = [
['iifname "eth0"', 'jump NAME_smoketest'],
['ip saddr { 172.16.99.0/24 }', 'ip daddr 172.16.10.10', 'th dport { 53, 123 }', 'return'],
- ['ether saddr { 00:01:02:03:04:05 }', 'return']
+ ['ether saddr { 00:01:02:03:04:05 }', 'return'],
+ ['set smoketest_domain'],
+ ['elements = { 192.0.2.5, 192.0.2.8,'],
+ ['192.0.2.10, 192.0.2.11 }'],
+ ['ip saddr @smoketest_domain', 'return']
]
nftables_output = cmd('sudo nft list table ip filter')
@@ -89,6 +103,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
break
self.assertTrue(matched, msg=search)
+ self.cli_delete(['system', 'static-host-mapping'])
+ self.cli_commit()
+
def test_basic_rules(self):
self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop'])
self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept'])
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 6924bf555..335098bf1 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2022 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
@@ -26,7 +26,13 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configdiff import get_config_diff, Diff
+from vyos.firewall import get_ips_domains_dict
+from vyos.firewall import nft_add_set_elements
+from vyos.firewall import nft_flush_set
+from vyos.firewall import nft_init_set
+from vyos.firewall import nft_update_set_elements
from vyos.template import render
+from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search_args
from vyos.util import process_named_running
@@ -408,6 +414,26 @@ def apply(firewall):
if install_result == 1:
raise ConfigError('Failed to apply firewall')
+ # set fireall group domain-group xxx
+ if 'group' in firewall:
+ if 'domain_group' in firewall['group']:
+ # T970 Enable a resolver (systemd daemon) that checks
+ # domain-group addresses and update entries for domains by timeout
+ # If router loaded without internet connection or for synchronization
+ call('systemctl restart vyos-domain-group-resolve.service')
+ for group, group_config in firewall['group']['domain_group'].items():
+ domains = []
+ for address in group_config['address']:
+ domains.append(address)
+ # Add elements to domain-group, try to resolve domain => ip
+ # and add elements to nft set
+ ip_dict = get_ips_domains_dict(domains)
+ elements = sum(ip_dict.values(), [])
+ nft_init_set(group)
+ nft_add_set_elements(group, elements)
+ else:
+ call('systemctl stop vyos-domain-group-resolve.service')
+
if 'state_policy' in firewall and not state_policy_rule_exists():
for chain in ['VYOS_FW_FORWARD', 'VYOS_FW_OUTPUT', 'VYOS_FW_LOCAL']:
cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY')
diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py
new file mode 100755
index 000000000..e8501cfc6
--- /dev/null
+++ b/src/helpers/vyos-domain-group-resolve.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 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 time
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.firewall import get_ips_domains_dict
+from vyos.firewall import nft_add_set_elements
+from vyos.firewall import nft_flush_set
+from vyos.firewall import nft_init_set
+from vyos.firewall import nft_update_set_elements
+from vyos.util import call
+
+
+base = ['firewall', 'group', 'domain-group']
+check_required = True
+# count_failed = 0
+# Timeout in sec between checks
+timeout = 300
+
+domain_state = {}
+
+if __name__ == '__main__':
+
+ while check_required:
+ config = ConfigTreeQuery()
+ if config.exists(base):
+ domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ for set_name, domain_config in domain_groups.items():
+ list_domains = domain_config['address']
+ elements = []
+ ip_dict = get_ips_domains_dict(list_domains)
+
+ for domain in list_domains:
+ # Resolution succeeded, update domain state
+ if domain in ip_dict:
+ domain_state[domain] = ip_dict[domain]
+ elements += ip_dict[domain]
+ # Resolution failed, use previous domain state
+ elif domain in domain_state:
+ elements += domain_state[domain]
+
+ # Resolve successful
+ if elements:
+ nft_update_set_elements(set_name, elements)
+ time.sleep(timeout)
diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service
new file mode 100644
index 000000000..29628fddb
--- /dev/null
+++ b/src/systemd/vyos-domain-group-resolve.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=VyOS firewall domain-group resolver
+After=vyos-router.service
+
+[Service]
+Type=simple
+Restart=always
+ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-domain-group-resolve.py
+
+[Install]
+WantedBy=multi-user.target