From 1b5bdef24632bbc497768847719eba8c33d93ef5 Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Thu, 5 Nov 2026 22:17:15 +0100 Subject: https: T9559: fix bogus port conflict with "vrf default" Committing "set service https vrf default" failed with "TCP port 443 in vrf default is already used by another service" although nothing listened on the port. The port check bound its test socket to a VRF device named "default", which does not exist, and reported that failure as a port conflict. The default VRF is now treated like no VRF at all. A VRF device that does not exist no longer counts as a busy port, as nothing can listen in it; any other error is raised instead of being reported as a conflict. The same check is used by the Prometheus VPP exporter. --- python/vyos/utils/network.py | 27 +++++++++++++++++++-------- smoketest/scripts/cli/test_service_https.py | 14 ++++++++++++++ src/tests/test_utils_network.py | 2 ++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 4a4414f56..d478f4d2c 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -394,6 +394,7 @@ def check_port_availability(address: str = None, port: int = 0, False if a port is busy or IP address does not exists True if a port is free and IP address exists """ + import errno import socket from ipaddress import ip_address @@ -417,19 +418,29 @@ def check_port_availability(address: str = None, port: int = 0, print(f'Invalid address: {address}') return False + # the default VRF has no master device - an unbound socket already uses it + if vrf == 'default': + vrf = None + for family, socktype, proto, canonname, sockaddr in addr_info: - try: - with socket.socket(family, socktype, proto) as s: - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # When a VRF is specified, bind the socket to the VRF master - # device so the address is resolved in that VRF's L3 domain. - if vrf: + with socket.socket(family, socktype, proto) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # When a VRF is specified, bind the socket to the VRF master + # device so the address is resolved in that VRF's L3 domain. + if vrf: + try: s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, (vrf + '\0').encode()) + except OSError as e: + # nothing can listen in a VRF that does not exist (yet) + if e.errno == errno.ENODEV: + return True + raise + try: s.bind(sockaddr) return True # port is free to use - except OSError: - return False # port is already in use + except OSError: + return False # port is already in use # if we reach this point, no socket was tested and we assume the port is # already in use - better safe then sorry diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index f432b4d57..f4176107a 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -252,6 +252,20 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.cli_delete(['interfaces', 'dummy', interface]) self.cli_delete(['vrf', 'name', vrf]) + def test_vrf_default(self): + # "vrf default" has no VRF master device and must behave like no VRF + # at all - the port check used to report a bogus port conflict + self.cli_set( + base_path + ['api', 'keys', 'id', 'key-01', 'key', 'MySuperSecretVyOS'] + ) + self.cli_set(base_path + ['vrf', 'default']) + self.cli_commit() + + self.assertTrue(process_named_running(PROCESS_NAME)) + ports = {c.laddr.port for c in psutil.net_connections(kind='tcp') + if c.status == psutil.CONN_LISTEN} + self.assertIn(443, ports) + def test_certificate(self): cert_name = 'test_https' dh_name = 'dh-test' diff --git a/src/tests/test_utils_network.py b/src/tests/test_utils_network.py index 708b6e064..83cf57ffc 100644 --- a/src/tests/test_utils_network.py +++ b/src/tests/test_utils_network.py @@ -61,6 +61,8 @@ class TestVyOSUtilsNetwork(TestCase): self.assertFalse(vyos.utils.network.check_port_availability('192.0.2.1', 443)) # We do not have 2001:db8::1 configured on this system self.assertFalse(vyos.utils.network.check_port_availability('2001:db8::1', 80, protocol='udp')) + # the default VRF is not a device and must behave like no VRF at all + self.assertTrue(vyos.utils.network.check_port_availability('127.0.0.1', 8080, vrf='default')) def test_normalize_prefix(self): normalize_prefix = vyos.utils.network.normalize_prefix -- cgit v1.2.3 From 0949562e1761bd3885664132e9485347afcc50aa Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Thu, 5 Nov 2026 21:57:14 +0000 Subject: https: T9559: report a missing VRF instead of a free port The port check reported a port as free when the VRF to test it in did not exist, so a missing or misspelled VRF passed unnoticed. The device error is now raised like any other error. Callers that verify the VRF beforehand are not affected; the default VRF is still treated like no VRF at all. --- python/vyos/utils/network.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index d478f4d2c..c7304d15b 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -394,7 +394,6 @@ def check_port_availability(address: str = None, port: int = 0, False if a port is busy or IP address does not exists True if a port is free and IP address exists """ - import errno import socket from ipaddress import ip_address @@ -428,14 +427,8 @@ def check_port_availability(address: str = None, port: int = 0, # When a VRF is specified, bind the socket to the VRF master # device so the address is resolved in that VRF's L3 domain. if vrf: - try: - s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, - (vrf + '\0').encode()) - except OSError as e: - # nothing can listen in a VRF that does not exist (yet) - if e.errno == errno.ENODEV: - return True - raise + s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, + (vrf + '\0').encode()) try: s.bind(sockaddr) return True # port is free to use -- cgit v1.2.3