diff options
| author | Christian Breunig <christian@breunig.cc> | 2026-04-03 13:05:22 +0200 |
|---|---|---|
| committer | Christian Breunig <christian@breunig.cc> | 2026-04-03 13:32:48 +0200 |
| commit | 33179967cd169d52c0732e5eed1aaa93ad12ab8d (patch) | |
| tree | 68332ec58b739a55316d7d67ffee5dbada5f5f19 | |
| parent | 316c45aaaee2e646f70b6ef444a462656d31f682 (diff) | |
| download | vyos-1x-33179967cd169d52c0732e5eed1aaa93ad12ab8d.tar.gz vyos-1x-33179967cd169d52c0732e5eed1aaa93ad12ab8d.zip | |
nat64: T8456: add constraint for translation port range
NAT64 requires dedicated transport address space for translation, similar to
exclusive port ownership during socket binding. If a local process and Jool
translation both use the same transport tuple (for example, 192.0.2.1:5000),
traffic conflicts can occur.
Jool does not prevent pool4 from overlapping with other port allocations, so
avoiding conflicts is an operator responsibility.
In addition to service ports already in use, account for Linux ephemeral
range (net.ipv4.ip_local_port_range), which defaults to 32768-60999. This
default is why, when pool4 is empty, Jool uses 61001-65535 on the node's
primary global addresses.
One can adjust the ephemeral range via sysctl, and Jools translation range via
pool4 add/remove.
vyos@vyos:~$ sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768 60999
VyOS now uses verify() in NAT64 to check that the supplied tranlation port
range does not overlap with the Kernels ephemeral port range.
| -rwxr-xr-x | smoketest/scripts/cli/test_nat64.py | 31 | ||||
| -rwxr-xr-x | src/conf_mode/nat64.py | 32 |
2 files changed, 47 insertions, 16 deletions
diff --git a/smoketest/scripts/cli/test_nat64.py b/smoketest/scripts/cli/test_nat64.py index 077648118..c578da214 100755 --- a/smoketest/scripts/cli/test_nat64.py +++ b/smoketest/scripts/cli/test_nat64.py @@ -20,6 +20,9 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError +from vyos.utils.system import sysctl_read + base_path = ['nat64'] src_path = base_path + ['source'] @@ -48,15 +51,25 @@ class TestNAT64(VyOSUnitTestSHIM.TestCase): pool = '192.0.2.10' pool_port = '1-65535' - self.cli_set(src_path + ['rule', rule, 'source', 'prefix', prefix_v6]) - self.cli_set( - src_path - + ['rule', rule, 'translation', 'pool', translation_rule, 'address', pool] - ) - self.cli_set( - src_path - + ['rule', rule, 'translation', 'pool', translation_rule, 'port', pool_port] - ) + rule_path = src_path + ['rule', rule] + self.cli_set(rule_path + ['source', 'prefix', prefix_v6]) + self.cli_set(rule_path + ['translation', 'pool', translation_rule, + 'address', pool]) + self.cli_set(rule_path + ['translation', 'pool', translation_rule, + 'port', pool_port]) + + # pool_port overlaps with the Linux Kernel ephemeral port range + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + # https://nicmx.github.io/Jool/en/usr-flags-pool4.html#port-range + # Jool is incapable of ensuring pool4 does not intersect with other defined + # port ranges; this validation is the operator’s responsibility. + tmp = sysctl_read('net.ipv4.ip_local_port_range') + _, ephemeral_port_max = map(int, tmp.split()) + pool_port = f'{ephemeral_port_max +1}-{ephemeral_port_max +1000}' + self.cli_set(rule_path + ['translation', 'pool', translation_rule, + 'port', pool_port]) self.cli_commit() # Load the JSON file diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py index c31806b43..175e9eb1e 100755 --- a/src/conf_mode/nat64.py +++ b/src/conf_mode/nat64.py @@ -34,6 +34,7 @@ from vyos.utils.kernel import check_kmod from vyos.utils.kernel import unload_kmod from vyos.utils.process import cmd from vyos.utils.process import run +from vyos.utils.system import sysctl_read airbag.enable() @@ -92,6 +93,12 @@ def verify(nat64) -> None: # nothing left to do return + # https://nicmx.github.io/Jool/en/usr-flags-pool4.html#port-range + # Jool is incapable of ensuring pool4 does not intersect with other defined + # port ranges; this validation is the operator’s responsibility. + tmp = sysctl_read('net.ipv4.ip_local_port_range') + ephemeral_port_min, ephemeral_port_max = map(int, tmp.split()) + if dict_search('source.rule', nat64): # Ensure only 1 netfilter instance per namespace nf_rules = filter( @@ -123,18 +130,29 @@ def verify(nat64) -> None: pools = dict_search('translation.pool', instance) if pools: for num, pool in pools.items(): + error_msg = f'Source NAT64 rule {rule} translation pool {num}' if 'address' not in pool: - raise ConfigError( - f'Source NAT64 rule {rule} translation pool ' - f'{num} missing address/prefix' - ) + raise ConfigError(f'{error_msg} missing address/prefix') if 'port' not in pool: + raise ConfigError(f'{error_msg} missing port(-range)') + # Split the provided ports, it's either a single port or start-end + tmp = pool['port'].split('-') + if len(tmp) == 1: # single port + pool_min = pool_max = int(tmp[0]) + elif len(tmp) == 2: # port range with start-stop + pool_min, pool_max = map(int, tmp) + else: + raise ConfigError('Invalid port range, this should not happen!') + + # Inclusive overlap check between nat64 translation ports and + # the Linux Kernel ephemeral port range + # overlap if pool_min <= ephemeral_port_max and ephemeral_port_min <= pool_max + if pool_min <= ephemeral_port_max and ephemeral_port_min <= pool_max: raise ConfigError( - f'Source NAT64 rule {rule} translation pool ' - f'{num} missing port(-range)' + f'{error_msg} port range {pool_min}-{pool_max} overlaps with ' + f'local ephemeral range {ephemeral_port_min}-{ephemeral_port_max}' ) - def generate(nat64) -> None: if not nat64: return |
