From 4c61fa82f59e26023993be56be1ff9bf0cb5251e Mon Sep 17 00:00:00 2001 From: Nicolas Fort Date: Wed, 19 Jul 2023 14:25:55 +0000 Subject: T4899: NAT Redirect: adddestination nat redirection (to local host) feature. --- interface-definitions/nat.xml.in | 8 ++++++++ python/vyos/nat.py | 42 +++++++++++++++++++++------------------ smoketest/scripts/cli/test_nat.py | 21 ++++++++++++++++++++ src/conf_mode/nat.py | 3 ++- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 501ff05d3..a06ceefb6 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -44,6 +44,14 @@ #include #include + + + Redirect to local host + + + #include + + diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 5b8d5d1a3..603fedb9b 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -54,28 +54,32 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): translation_str = 'return' log_suffix = '-EXCL' elif 'translation' in rule_conf: - translation_prefix = nat_type[:1] - translation_output = [f'{translation_prefix}nat'] addr = dict_search_args(rule_conf, 'translation', 'address') port = dict_search_args(rule_conf, 'translation', 'port') - - if addr and is_ip_network(addr): - if not ipv6: - map_addr = dict_search_args(rule_conf, nat_type, 'address') - translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}') - ignore_type_addr = True - else: - translation_output.append(f'prefix to {addr}') - elif addr == 'masquerade': - if port: - addr = f'{addr} to ' - translation_output = [addr] - log_suffix = '-MASQ' + redirect_port = dict_search_args(rule_conf, 'translation', 'redirect', 'port') + if redirect_port: + translation_output = [f'redirect to {redirect_port}'] else: - translation_output.append('to') - if addr: - addr = bracketize_ipv6(addr) - translation_output.append(addr) + translation_prefix = nat_type[:1] + translation_output = [f'{translation_prefix}nat'] + + if addr and is_ip_network(addr): + if not ipv6: + map_addr = dict_search_args(rule_conf, nat_type, 'address') + translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}') + ignore_type_addr = True + else: + translation_output.append(f'prefix to {addr}') + elif addr == 'masquerade': + if port: + addr = f'{addr} to ' + translation_output = [addr] + log_suffix = '-MASQ' + else: + translation_output.append('to') + if addr: + addr = bracketize_ipv6(addr) + translation_output.append(addr) options = [] addr_mapping = dict_search_args(rule_conf, 'translation', 'options', 'address_mapping') diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 02fa03f7b..28d566eba 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -231,5 +231,26 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_static_nat') + def test_dnat_redirect(self): + dst_addr_1 = '10.0.1.1' + dest_port = '5122' + protocol = 'tcp' + redirected_port = '22' + ifname = 'eth0' + + self.cli_set(dst_path + ['rule', '10', 'destination', 'address', dst_addr_1]) + self.cli_set(dst_path + ['rule', '10', 'destination', 'port', dest_port]) + self.cli_set(dst_path + ['rule', '10', 'protocol', protocol]) + self.cli_set(dst_path + ['rule', '10', 'inbound-interface', ifname]) + self.cli_set(dst_path + ['rule', '10', 'translation', 'redirect', 'port', redirected_port]) + + self.cli_commit() + + nftables_search = [ + [f'iifname "{ifname}"', f'ip daddr {dst_addr_1}', f'{protocol} dport {dest_port}', f'redirect to :{redirected_port}'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_nat') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 5f4b658f8..e19b12937 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -72,6 +72,7 @@ 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 + dict_search('translation.redirect.port', config) != None or dict_search('destination.port', config) != None or dict_search('source.port', config)): @@ -221,7 +222,7 @@ def verify(nat): elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') - if not dict_search('translation.address', config) and not dict_search('translation.port', config): + if not dict_search('translation.address', config) and not dict_search('translation.port', config) and not dict_search('translation.redirect.port', config): if 'exclude' not in config: raise ConfigError(f'{err_msg} translation requires address and/or port') -- cgit v1.2.3