Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

firewall: T5493: Implement remote-group #4326

Open
wants to merge 1 commit into
base: current
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions data/templates/firewall/nftables-defines.j2
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
}
{% endfor %}
{% endif %}
{% if group.remote_group is vyos_defined and is_l3 and not is_ipv6 %}
{% for name, name_config in group.remote_group.items() %}
set R_{{ name }} {
type {{ ip_type }}
flags interval
auto-merge
}
{% endfor %}
{% endif %}
{% if group.mac_group is vyos_defined %}
{% for group_name, group_conf in group.mac_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
Expand Down
13 changes: 13 additions & 0 deletions interface-definitions/firewall.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@
</tagNode>
</children>
</node>
<tagNode name="remote-group">
<properties>
<help>Firewall remote-group</help>
<constraint>
#include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of firewall group can only contain alphanumeric letters, hyphen, underscores and dot</constraintErrorMessage>
</properties>
<children>
#include <include/url-http-https.xml.i>
#include <include/generic-description.xml.i>
</children>
</tagNode>
<tagNode name="interface-group">
<properties>
<help>Firewall interface-group</help>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
#include <include/firewall/source-destination-dynamic-group.xml.i>
#include <include/firewall/source-destination-remote-group.xml.i>
</children>
</node>
<leafNode name="jump-target">
Expand All @@ -39,6 +40,7 @@
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
#include <include/firewall/source-destination-dynamic-group.xml.i>
#include <include/firewall/source-destination-remote-group.xml.i>
</children>
</node>
<!-- include end -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- include start from firewall/source-destination-remote-group.xml.i -->
<node name="group">
<properties>
<help>Group</help>
</properties>
<children>
<leafNode name="remote-group">
<properties>
<help>Group of remote addresses</help>
<completionHelp>
<path>firewall group remote-group</path>
</completionHelp>
</properties>
</leafNode>
</children>
</node>
<!-- include end -->
7 changes: 7 additions & 0 deletions python/vyos/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,13 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}')
elif 'remote_group' in group:
group_name = group['remote_group']
operator = ''
if group_name[0] == '!':
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}')
if 'mac_group' in group:
group_name = group['mac_group']
operator = ''
Expand Down
16 changes: 16 additions & 0 deletions python/vyos/utils/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,3 +599,19 @@ def get_nft_vrf_zone_mapping() -> dict:
for (vrf_name, vrf_id) in vrf_list:
output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id})
return output

def is_valid_ipv4_address_or_range(addr: str) -> bool:
"""
Validates if the provided address is a valid IPv4, CIDR or IPv4 range
:param addr: address to test
:return: bool: True if provided address is valid
"""
from ipaddress import ip_network
try:
if '-' in addr: # If we are checking a range, validate both address's individually
split = addr.split('-')
return is_valid_ipv4_address_or_range(split[0]) and is_valid_ipv4_address_or_range(split[1])
else:
return ip_network(addr).version == 4
except:
return False
34 changes: 34 additions & 0 deletions smoketest/scripts/cli/test_firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,5 +1262,39 @@ def test_gre_match(self):
with self.assertRaises(ConfigSessionError):
self.cli_commit()

def test_ipv4_remote_group(self):
# Setup base config for test
self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])

self.cli_commit()

# Test remote-group had been loaded correctly in nft
nftables_search = [
['R_group01'],
['type ipv4_addr'],
['flags interval'],
['meta l4proto', 'daddr @R_group01', "ipv4-INP-filter-10"]
]
self.verify_nftables(nftables_search, 'ip vyos_filter')

# Test remote-group cannot be configured without a URL
self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url'])

with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_discard()

# Test remote-group cannot be set alongside address in rules
self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'address', '127.0.0.1'])

with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_discard()


if __name__ == '__main__':
unittest.main(verbosity=2)
16 changes: 11 additions & 5 deletions src/conf_mode/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
'network_group',
'port_group',
'interface_group',
## Added for group ussage in bridge firewall
'remote_group',
## Added for group usage in bridge firewall
'ipv4_address_group',
'ipv6_address_group',
'ipv4_network_group',
Expand Down Expand Up @@ -311,8 +312,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
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')
if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1:
raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified')

for group in valid_groups:
if group in side_conf['group']:
Expand All @@ -332,7 +333,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):

error_group = fw_group.replace("_", "-")

if group in ['address_group', 'network_group', 'domain_group']:
if group in ['address_group', 'network_group', 'domain_group', 'remote_group']:
types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf]
if types:
raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
Expand Down Expand Up @@ -442,6 +443,11 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])

if 'remote_group' in firewall['group']:
for group_name, group in firewall['group']['remote_group'].items():
if 'url' not in group:
raise ConfigError(f'remote-group {group_name} must have a url configured')

for family in ['ipv4', 'ipv6', 'bridge']:
if family in firewall:
for chain in ['name','forward','input','output', 'prerouting']:
Expand Down Expand Up @@ -598,7 +604,7 @@ def apply(firewall):

## DOMAIN RESOLVER
domain_action = 'restart'
if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n'
Path(domain_resolver_usage).write_text(text)
else:
Expand Down
34 changes: 20 additions & 14 deletions src/op_mode/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
if not source_addr:
source_addr = dict_search_args(rule_conf, 'source', 'group', 'domain_group')
if not source_addr:
source_addr = dict_search_args(rule_conf, 'source', 'fqdn')
source_addr = dict_search_args(rule_conf, 'source', 'group', 'remote_group')
if not source_addr:
source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code')
if source_addr:
source_addr = str(source_addr)[1:-1].replace('\'','')
if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'):
source_addr = 'NOT ' + str(source_addr)
source_addr = dict_search_args(rule_conf, 'source', 'fqdn')
if not source_addr:
source_addr = 'any'
source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code')
if source_addr:
source_addr = str(source_addr)[1:-1].replace('\'','')
if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'):
source_addr = 'NOT ' + str(source_addr)
if not source_addr:
source_addr = 'any'

# Get destination
dest_addr = dict_search_args(rule_conf, 'destination', 'address')
Expand All @@ -272,15 +274,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
if not dest_addr:
dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'domain_group')
if not dest_addr:
dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn')
dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'remote_group')
if not dest_addr:
dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code')
if dest_addr:
dest_addr = str(dest_addr)[1:-1].replace('\'','')
if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'):
dest_addr = 'NOT ' + str(dest_addr)
dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn')
if not dest_addr:
dest_addr = 'any'
dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code')
if dest_addr:
dest_addr = str(dest_addr)[1:-1].replace('\'','')
if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'):
dest_addr = 'NOT ' + str(dest_addr)
if not dest_addr:
dest_addr = 'any'

# Get inbound interface
iiface = dict_search_args(rule_conf, 'inbound_interface', 'name')
Expand Down Expand Up @@ -571,6 +575,8 @@ def find_references(group_type, group_name):
row.append("\n".join(sorted(group_conf['port'])))
elif 'interface' in group_conf:
row.append("\n".join(sorted(group_conf['interface'])))
elif 'url' in group_conf:
row.append(group_conf['url'])
else:
row.append('N/D')
rows.append(row)
Expand Down
58 changes: 57 additions & 1 deletion src/services/vyos-domain-resolver
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@
#
# 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 json
import time
import logging
import os

from vyos.configdict import dict_merge
from vyos.configquery import ConfigTreeQuery
from vyos.firewall import fqdn_config_parse
from vyos.firewall import fqdn_resolve
from vyos.ifconfig import WireGuardIf
from vyos.remote import download
from vyos.utils.commit import commit_in_progress
from vyos.utils.dict import dict_search_args
from vyos.utils.kernel import WIREGUARD_REKEY_AFTER_TIME
from vyos.utils.file import makedir, chmod_775, write_file, read_file
from vyos.utils.network import is_valid_ipv4_address_or_range
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.xml_ref import get_defaults
Expand All @@ -37,6 +40,8 @@ base_firewall = ['firewall']
base_nat = ['nat']
base_interfaces = ['interfaces']

firewall_config_dir = "/config/firewall"

domain_state = {}

ipv4_tables = {
Expand Down Expand Up @@ -119,6 +124,56 @@ def nft_valid_sets():
except:
return []

def update_remote_group(config):
conf_lines = []
count = 0
valid_sets = nft_valid_sets()

remote_groups = dict_search_args(config, 'group', 'remote_group')
if remote_groups:
# Create directory for list files if necessary
if not os.path.isdir(firewall_config_dir):
makedir(firewall_config_dir, group='vyattacfg')
chmod_775(firewall_config_dir)

for set_name, remote_config in remote_groups.items():
if 'url' not in remote_config:
continue
nft_set_name = f'R_{set_name}'

# Create list file if necessary
list_file = os.path.join(firewall_config_dir, f"{nft_set_name}.txt")
if not os.path.exists(list_file):
write_file(list_file, '', user="root", group="vyattacfg", mode=0o644)

# Attempt to download file, use cached version if download fails
try:
download(list_file, remote_config['url'])
except:
logger.error(f'Failed to download list-file for {set_name} remote group')
logger.info(f'Using cached list-file for {set_name} remote group')

# Read list file
ip_list = []
for line in read_file(list_file).splitlines():
line_first_word = line.strip().partition(' ')[0]

if is_valid_ipv4_address_or_range(line_first_word):
ip_list.append(line_first_word)

# Load tables
for table in ipv4_tables:
if (table, nft_set_name) in valid_sets:
conf_lines += nft_output(table, nft_set_name, ip_list)

count += 1

nft_conf_str = "\n".join(conf_lines) + "\n"
code = run(f'nft --file -', input=nft_conf_str)

logger.info(f'Updated {count} remote-groups in firewall - result: {code}')


def update_fqdn(config, node):
conf_lines = []
count = 0
Expand Down Expand Up @@ -232,5 +287,6 @@ if __name__ == '__main__':
while True:
update_fqdn(firewall, 'firewall')
update_fqdn(nat, 'nat')
update_remote_group(firewall)
update_interfaces(interfaces, 'interfaces')
time.sleep(timeout)
Loading