From 33179967cd169d52c0732e5eed1aaa93ad12ab8d Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Fri, 3 Apr 2026 13:05:22 +0200 Subject: 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. --- src/conf_mode/nat64.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) (limited to 'src') 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 -- cgit v1.2.3