summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/include/firewall/firewall-hashing-parameters.xml.i35
-rw-r--r--interface-definitions/include/firewall/nat-balance.xml.i28
-rw-r--r--interface-definitions/include/nat-rule.xml.i9
-rw-r--r--python/vyos/nat.py33
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py36
-rwxr-xr-xsrc/conf_mode/nat.py17
6 files changed, 155 insertions, 3 deletions
diff --git a/interface-definitions/include/firewall/firewall-hashing-parameters.xml.i b/interface-definitions/include/firewall/firewall-hashing-parameters.xml.i
new file mode 100644
index 000000000..7f34de3ba
--- /dev/null
+++ b/interface-definitions/include/firewall/firewall-hashing-parameters.xml.i
@@ -0,0 +1,35 @@
+<!-- include start from firewall/firewall-hashing-parameters.xml.i -->
+<leafNode name="hash">
+ <properties>
+ <help>Define the parameters of the packet header to apply the hashing</help>
+ <completionHelp>
+ <list>source-address destination-address source-port destination-port random</list>
+ </completionHelp>
+ <valueHelp>
+ <format>source-address</format>
+ <description>Use source IP address for hashing</description>
+ </valueHelp>
+ <valueHelp>
+ <format>destination-address</format>
+ <description>Use destination IP address for hashing</description>
+ </valueHelp>
+ <valueHelp>
+ <format>source-port</format>
+ <description>Use source port for hashing</description>
+ </valueHelp>
+ <valueHelp>
+ <format>destination-port</format>
+ <description>Use destination port for hashing</description>
+ </valueHelp>
+ <valueHelp>
+ <format>random</format>
+ <description>Do not use information from ip header. Use random value.</description>
+ </valueHelp>
+ <constraint>
+ <regex>(source-address|destination-address|source-port|destination-port|random)</regex>
+ </constraint>
+ <multi/>
+ </properties>
+ <defaultValue>random</defaultValue>
+</leafNode>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/nat-balance.xml.i b/interface-definitions/include/firewall/nat-balance.xml.i
new file mode 100644
index 000000000..01793f06b
--- /dev/null
+++ b/interface-definitions/include/firewall/nat-balance.xml.i
@@ -0,0 +1,28 @@
+<!-- include start from firewall/nat-balance.xml.i -->
+<tagNode name="backend">
+ <properties>
+ <help>Translated IP address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="weight">
+ <properties>
+ <help>Set probability for this output value</help>
+ <valueHelp>
+ <format>u32:1-100</format>
+ <description>Set probability for this output value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--allow-range --range 1-100"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</tagNode>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i
index 7b3b8804e..6234e6195 100644
--- a/interface-definitions/include/nat-rule.xml.i
+++ b/interface-definitions/include/nat-rule.xml.i
@@ -25,6 +25,15 @@
</node>
#include <include/generic-disable-node.xml.i>
#include <include/nat-exclude.xml.i>
+ <node name="load-balance">
+ <properties>
+ <help>Apply NAT load balance</help>
+ </properties>
+ <children>
+ #include <include/firewall/firewall-hashing-parameters.xml.i>
+ #include <include/firewall/nat-balance.xml.i>
+ </children>
+ </node>
<leafNode name="log">
<properties>
<help>NAT rule logging</help>
diff --git a/python/vyos/nat.py b/python/vyos/nat.py
index 603fedb9b..418efe649 100644
--- a/python/vyos/nat.py
+++ b/python/vyos/nat.py
@@ -94,6 +94,39 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
if options:
translation_str += f' {",".join(options)}'
+ if 'backend' in rule_conf['load_balance']:
+ hash_input_items = []
+ current_prob = 0
+ nat_map = []
+
+ for trans_addr, addr in rule_conf['load_balance']['backend'].items():
+ item_prob = int(addr['weight'])
+ upper_limit = current_prob + item_prob - 1
+ hash_val = str(current_prob) + '-' + str(upper_limit)
+ element = hash_val + " : " + trans_addr
+ nat_map.append(element)
+ current_prob = current_prob + item_prob
+
+ elements = ' , '.join(nat_map)
+
+ if 'hash' in rule_conf['load_balance'] and 'random' in rule_conf['load_balance']['hash']:
+ translation_str += ' numgen random mod 100 map ' + '{ ' + f'{elements}' + ' }'
+ else:
+ for input_param in rule_conf['load_balance']['hash']:
+ if input_param == 'source-address':
+ param = 'ip saddr'
+ elif input_param == 'destination-address':
+ param = 'ip daddr'
+ elif input_param == 'source-port':
+ prot = rule_conf['protocol']
+ param = f'{prot} sport'
+ elif input_param == 'destination-port':
+ prot = rule_conf['protocol']
+ param = f'{prot} dport'
+ hash_input_items.append(param)
+ hash_input = ' . '.join(hash_input_items)
+ translation_str += f' jhash ' + f'{hash_input}' + ' mod 100 map ' + '{ ' + f'{elements}' + ' }'
+
for target in ['source', 'destination']:
if target not in rule_conf:
continue
diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py
index 28d566eba..e6eaedeff 100755
--- a/smoketest/scripts/cli/test_nat.py
+++ b/smoketest/scripts/cli/test_nat.py
@@ -252,5 +252,41 @@ class TestNAT(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip vyos_nat')
+ def test_nat_balance(self):
+ ifname = 'eth0'
+ member_1 = '198.51.100.1'
+ weight_1 = '10'
+ member_2 = '198.51.100.2'
+ weight_2 = '90'
+ member_3 = '192.0.2.1'
+ weight_3 = '35'
+ member_4 = '192.0.2.2'
+ weight_4 = '65'
+ dst_port = '443'
+
+ self.cli_set(dst_path + ['rule', '1', 'inbound-interface', ifname])
+ self.cli_set(dst_path + ['rule', '1', 'protocol', 'tcp'])
+ self.cli_set(dst_path + ['rule', '1', 'destination', 'port', dst_port])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'source-address'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'source-port'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'destination-address'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'hash', 'destination-port'])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_1, 'weight', weight_1])
+ self.cli_set(dst_path + ['rule', '1', 'load-balance', 'backend', member_2, 'weight', weight_2])
+
+ self.cli_set(src_path + ['rule', '1', 'outbound-interface', ifname])
+ self.cli_set(src_path + ['rule', '1', 'load-balance', 'hash', 'random'])
+ self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_3, 'weight', weight_3])
+ self.cli_set(src_path + ['rule', '1', 'load-balance', 'backend', member_4, 'weight', weight_4])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'iifname "{ifname}"', f'tcp dport {dst_port}', f'dnat to jhash ip saddr . tcp sport . ip daddr . tcp dport mod 100 map', f'0-9 : {member_1}, 10-99 : {member_2}'],
+ [f'oifname "{ifname}"', f'snat to numgen random mod 100 map', f'0-34 : {member_3}, 35-99 : {member_4}']
+ ]
+
+ 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 e19b12937..5af06ed16 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -126,6 +126,18 @@ def verify_rule(config, err_msg, groups_dict):
if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port-group')
+ if 'load_balance' in config:
+ for item in ['source-port', 'destination-port']:
+ if item in config['load_balance']['hash'] and config['protocol'] not in ['tcp', 'udp']:
+ raise ConfigError('Protocol must be tcp or udp when specifying hash ports')
+ count = 0
+ if 'backend' in config['load_balance']:
+ for member in config['load_balance']['backend']:
+ weight = config['load_balance']['backend'][member]['weight']
+ count = count + int(weight)
+ if count != 100:
+ Warning(f'Sum of weight for nat load balance rule is not 100. You may get unexpected behaviour')
+
def get_config(config=None):
if config:
conf = config
@@ -199,7 +211,7 @@ def verify(nat):
Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
if not dict_search('translation.address', config) and not dict_search('translation.port', config):
- if 'exclude' not in config:
+ if 'exclude' not in config and 'backend' not in config['load_balance']:
raise ConfigError(f'{err_msg} translation requires address and/or port')
addr = dict_search('translation.address', config)
@@ -211,7 +223,6 @@ def verify(nat):
# common rule verification
verify_rule(config, err_msg, nat['firewall_group'])
-
if dict_search('destination.rule', nat):
for rule, config in dict_search('destination.rule', nat).items():
err_msg = f'Destination NAT configuration error in rule {rule}:'
@@ -223,7 +234,7 @@ def verify(nat):
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) and not dict_search('translation.redirect.port', config):
- if 'exclude' not in config:
+ if 'exclude' not in config and 'backend' not in config['load_balance']:
raise ConfigError(f'{err_msg} translation requires address and/or port')
# common rule verification