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

T6687: add fqdn support to nat rules. #4024

Merged
merged 1 commit into from
Sep 30, 2024
Merged
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
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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FQDN should be multi, isn't it?

vyos@r14# set nat source rule 100 destination fqdn example.com
[edit]
vyos@r14# set nat source rule 100 destination fqdn n1.example.com
[edit]
vyos@r14# set nat source rule 100 destination fqdn n2.example.com
[edit]
vyos@r14# set nat source rule 100 destination fqdn n3.example.com
[edit]
vyos@r14# show nat source rule 100 destination 
+fqdn n3.example.com
[edit]
vyos@r14# 

one FQDN per rule is inconvenient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should steer users to use a group for multiple per rule.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this is analogous to the firewall syntax, which supports either a group (including a domain group) or a single FQDN. Domain groups are supported in NAT.

#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 @@ -236,6 +236,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
Loading