Skip to content

Commit

Permalink
Merge pull request #4024 from nicolas-fort/T6687
Browse files Browse the repository at this point in the history
T6687: add fqdn support to nat rules.
  • Loading branch information
dmbaturin authored Sep 30, 2024
2 parents 0195aa1 + 4c3d037 commit eb55965
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 77 deletions.
13 changes: 13 additions & 0 deletions data/templates/firewall/nftables-nat.j2
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ table ip vyos_nat {
{% endfor %}
{% endif %}
}
{% for set_name in ip_fqdn %}
set FQDN_nat_{{ set_name }} {
type ipv4_addr
flags interval
}
{% endfor %}

#
# Source NAT rules build up here
Expand All @@ -31,7 +37,14 @@ table ip vyos_nat {
{{ config | nat_rule(rule, 'source') }}
{% endfor %}
{% endif %}

}
{% for set_name in ip_fqdn %}
set FQDN_nat_{{ set_name }} {
type ipv4_addr
flags interval
}
{% endfor %}

chain VYOS_PRE_DNAT_HOOK {
return
Expand Down
2 changes: 2 additions & 0 deletions interface-definitions/include/nat-rule.xml.i
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<help>NAT destination parameters</help>
</properties>
<children>
#include <include/firewall/fqdn.xml.i>
#include <include/nat-address.xml.i>
#include <include/nat-port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
Expand Down Expand Up @@ -315,6 +316,7 @@
<help>NAT source parameters</help>
</properties>
<children>
#include <include/firewall/fqdn.xml.i>
#include <include/nat-address.xml.i>
#include <include/nat-port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
Expand Down
47 changes: 26 additions & 21 deletions python/vyos/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,25 +53,32 @@ def conntrack_required(conf):

# Domain Resolver

def fqdn_config_parse(firewall):
firewall['ip_fqdn'] = {}
firewall['ip6_fqdn'] = {}

for domain, path in dict_search_recursive(firewall, 'fqdn'):
hook_name = path[1]
priority = path[2]

fw_name = path[2]
rule = path[4]
suffix = path[5][0]
set_name = f'{hook_name}_{priority}_{rule}_{suffix}'

if (path[0] == 'ipv4') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name'):
firewall['ip_fqdn'][set_name] = domain
elif (path[0] == 'ipv6') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name'):
if path[1] == 'name':
set_name = f'name6_{priority}_{rule}_{suffix}'
firewall['ip6_fqdn'][set_name] = domain
def fqdn_config_parse(config, node):
config['ip_fqdn'] = {}
config['ip6_fqdn'] = {}

for domain, path in dict_search_recursive(config, 'fqdn'):
if node != 'nat':
hook_name = path[1]
priority = path[2]

rule = path[4]
suffix = path[5][0]
set_name = f'{hook_name}_{priority}_{rule}_{suffix}'

if (path[0] == 'ipv4') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name'):
config['ip_fqdn'][set_name] = domain
elif (path[0] == 'ipv6') and (path[1] == 'forward' or path[1] == 'input' or path[1] == 'output' or path[1] == 'name'):
if path[1] == 'name':
set_name = f'name6_{priority}_{rule}_{suffix}'
config['ip6_fqdn'][set_name] = domain
else:
# Parse FQDN for NAT
nat_direction = path[0]
nat_rule = path[2]
suffix = path[3][0]
set_name = f'{nat_direction}_{nat_rule}_{suffix}'
config['ip_fqdn'][set_name] = domain

def fqdn_resolve(fqdn, ipv6=False):
try:
Expand All @@ -80,8 +87,6 @@ def fqdn_resolve(fqdn, ipv6=False):
except:
return None

# End Domain Resolver

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 --handle list chain {table} {chain}').split("\n")
Expand Down
7 changes: 7 additions & 0 deletions python/vyos/nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):

output.append(f'{proto} {prefix}port {operator} @P_{group_name}')

if 'fqdn' in side_conf:
fqdn = side_conf['fqdn']
operator = ''
if fqdn[0] == '!':
operator = '!='
output.append(f' ip {prefix}addr {operator} @FQDN_nat_{nat_type}_{rule_id}_{prefix}')

output.append('counter')

if 'log' in rule_conf:
Expand Down
26 changes: 26 additions & 0 deletions smoketest/scripts/cli/test_nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,5 +304,31 @@ def test_snat_net_port_map(self):

self.verify_nftables(nftables_search, 'ip vyos_nat')

def test_nat_fqdn(self):
source_domain = 'vyos.dev'
destination_domain = 'vyos.io'

self.cli_set(src_path + ['rule', '1', 'outbound-interface', 'name', 'eth0'])
self.cli_set(src_path + ['rule', '1', 'source', 'fqdn', source_domain])
self.cli_set(src_path + ['rule', '1', 'translation', 'address', 'masquerade'])

self.cli_set(dst_path + ['rule', '1', 'destination', 'fqdn', destination_domain])
self.cli_set(dst_path + ['rule', '1', 'source', 'fqdn', source_domain])
self.cli_set(dst_path + ['rule', '1', 'destination', 'port', '5122'])
self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp'])
self.cli_set(dst_path + ['rule', '1', 'translation', 'address', '198.51.100.1'])
self.cli_set(dst_path + ['rule', '1', 'translation', 'port', '22'])


self.cli_commit()

nftables_search = [
['set FQDN_nat_destination_1_d'],
['set FQDN_nat_source_1_s'],
['oifname "eth0"', 'ip saddr @FQDN_nat_source_1_s', 'masquerade', 'comment "SRC-NAT-1"'],
['tcp dport 5122', 'ip saddr @FQDN_nat_destination_1_s', 'ip daddr @FQDN_nat_destination_1_d', 'dnat to 198.51.100.1:22', 'comment "DST-NAT-1"']
]

self.verify_nftables(nftables_search, 'ip vyos_nat')
if __name__ == '__main__':
unittest.main(verbosity=2)
21 changes: 14 additions & 7 deletions src/conf_mode/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@
from vyos.utils.process import rc_cmd
from vyos import ConfigError
from vyos import airbag
from pathlib import Path
from subprocess import run as subp_run

airbag.enable()

nftables_conf = '/run/nftables.conf'
domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall'
domain_resolver_usage_nat = '/run/use-vyos-domain-resolver-nat'

sysctl_file = r'/run/sysctl/10-vyos-firewall.conf'

valid_groups = [
Expand Down Expand Up @@ -128,7 +132,7 @@ def get_config(config=None):

firewall['geoip_updated'] = geoip_updated(conf, firewall)

fqdn_config_parse(firewall)
fqdn_config_parse(firewall, 'firewall')

set_dependents('conntrack', conf)

Expand Down Expand Up @@ -570,12 +574,15 @@ def apply(firewall):

call_dependents()

# T970 Enable a resolver (systemd daemon) that checks
# 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'
## DOMAIN RESOLVER
domain_action = 'restart'
if 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:
Path(domain_resolver_usage).unlink(missing_ok=True)
if not Path('/run').glob('use-vyos-domain-resolver*'):
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')

if firewall['geoip_updated']:
Expand Down
20 changes: 20 additions & 0 deletions src/conf_mode/nat.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@
from vyos.utils.kernel import check_kmod
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.file import write_file
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.utils.process import call
from vyos.utils.network import is_addr_assigned
from vyos.utils.network import interface_exists
from vyos.firewall import fqdn_config_parse
from vyos import ConfigError

from vyos import airbag
Expand All @@ -39,6 +42,8 @@

nftables_nat_config = '/run/nftables_nat.conf'
nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
domain_resolver_usage = '/run/use-vyos-domain-resolver-nat'
domain_resolver_usage_firewall = '/run/use-vyos-domain-resolver-firewall'

valid_groups = [
'address_group',
Expand Down Expand Up @@ -71,6 +76,8 @@ def get_config(config=None):
if 'dynamic_group' in nat['firewall_group']:
del nat['firewall_group']['dynamic_group']

fqdn_config_parse(nat, 'nat')

return nat

def verify_rule(config, err_msg, groups_dict):
Expand Down Expand Up @@ -251,6 +258,19 @@ def apply(nat):

call_dependents()

# DOMAIN RESOLVER
if nat and 'deleted' not in nat:
domain_action = 'restart'
if nat['ip_fqdn'].items():
text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n'
write_file(domain_resolver_usage, text)
elif os.path.exists(domain_resolver_usage):
os.unlink(domain_resolver_usage)
if not os.path.exists(domain_resolver_usage_firewall):
# Firewall not using domain resolver
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')

return None

if __name__ == '__main__':
Expand Down
Loading

0 comments on commit eb55965

Please sign in to comment.