summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Navarro <crshman@gmail.com>2026-06-18 19:13:04 -0700
committerRobert Navarro <crshman@gmail.com>2026-06-23 21:49:41 -0700
commit368b0468bf4f1eaac8e0b61d0790206048eeb79a (patch)
treec202480d379046f19adb6736d6791f182295d3e7
parente0114625c08a2a9826822958f53724995580716d (diff)
downloadvyos-1x-368b0468bf4f1eaac8e0b61d0790206048eeb79a.tar.gz
vyos-1x-368b0468bf4f1eaac8e0b61d0790206048eeb79a.zip
ipsec: T8975: lock vti-up-down state DB to prevent lost updates under concurrent rekey
The vti-up-down hook and vpn_ipsec.py modify a flat-file DB (/tmp/ipsec_vti_interfaces) through three context managers that do an unlocked read-modify-write. During a coordinated rekey, strongSwan fires the hook for many VTIs concurrently, so the writers lost-update each other: an interface whose up-client add is overwritten is left admin-down while its CHILD_SA stays installed. Serialise all DB access by reusing vyos.utils.locking.Lock. A new _vti_updown_db_lock() context manager wraps the three public context managers, and remove_vti_updown_db() holds the lock across both the DB processing and the os.unlink() to close the create/delete race. Make the helpers absence-safe under the lock so callers no longer compose a separate existence check with a locked operation: open_vti_updown_db_readonly() yields None when the DB does not exist and remove_vti_updown_db() is a no-op when it is absent. Drop the now-redundant unlocked vti_updown_db_exists() pre-checks in vti.py and vpn_ipsec.py and handle the None yield.
-rw-r--r--python/vyos/ifconfig/vti.py8
-rw-r--r--python/vyos/utils/vti_updown_db.py74
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py6
3 files changed, 49 insertions, 39 deletions
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
index 030aa1ed7..01d724593 100644
--- a/python/vyos/ifconfig/vti.py
+++ b/python/vyos/ifconfig/vti.py
@@ -15,7 +15,7 @@
from vyos.ifconfig.interface import Interface
from vyos.utils.dict import dict_search
-from vyos.utils.vti_updown_db import vti_updown_db_exists, open_vti_updown_db_readonly
+from vyos.utils.vti_updown_db import open_vti_updown_db_readonly
@Interface.register
class VTIIf(Interface):
@@ -64,14 +64,14 @@ class VTIIf(Interface):
"""
Set interface administrative state to be 'up' or 'down'.
- The interface will only be brought 'up' if ith is attached to an
+ The interface will only be brought 'up' if it is attached to an
active ipsec site-to-site connection or remote access connection.
"""
if state == 'down' or self.bypass_vti_updown_db:
super().set_admin_state(state)
- elif vti_updown_db_exists():
+ else:
with open_vti_updown_db_readonly() as db:
- if db.wantsInterfaceUp(self.ifname):
+ if db is not None and db.wantsInterfaceUp(self.ifname):
super().set_admin_state(state)
def get_mac(self):
diff --git a/python/vyos/utils/vti_updown_db.py b/python/vyos/utils/vti_updown_db.py
index 2931df86c..390db5195 100644
--- a/python/vyos/utils/vti_updown_db.py
+++ b/python/vyos/utils/vti_updown_db.py
@@ -18,55 +18,67 @@ import os
from contextlib import contextmanager
from syslog import syslog
+from vyos.utils.locking import Lock
+
VTI_WANT_UP_IFLIST = '/tmp/ipsec_vti_interfaces'
+VTI_UPDOWN_LOCK_NAME = 'ipsec_vti_updown'
def vti_updown_db_exists():
""" Returns true if the database exists """
return os.path.exists(VTI_WANT_UP_IFLIST)
@contextmanager
-def open_vti_updown_db_for_create_or_update():
- """ Opens the database for reading and writing, creating the database if it does not exist """
- if vti_updown_db_exists():
- f = open(VTI_WANT_UP_IFLIST, 'r+')
- else:
- f = open(VTI_WANT_UP_IFLIST, 'x+')
+def _vti_updown_db_lock():
+ """Serialise access to the VTI up/down DB across the concurrent updown-hook
+ invocations (one per VTI) that strongSwan fires during a coordinated rekey,
+ which would otherwise lost-update the shared state file."""
+ lock = Lock(VTI_UPDOWN_LOCK_NAME)
+ lock.acquire() # timeout=0 -> block until acquired
try:
- db = VTIUpDownDB(f)
- yield db
+ yield
finally:
- f.close()
+ lock.release()
+
+
+@contextmanager
+def open_vti_updown_db_for_create_or_update():
+ """ Opens the database for reading and writing, creating the database if it does not exist """
+ with _vti_updown_db_lock():
+ mode = 'r+' if vti_updown_db_exists() else 'x+'
+ with open(VTI_WANT_UP_IFLIST, mode) as f:
+ yield VTIUpDownDB(f)
@contextmanager
def open_vti_updown_db_for_update():
""" Opens the database for reading and writing, returning an error if it does not exist """
- f = open(VTI_WANT_UP_IFLIST, 'r+')
- try:
- db = VTIUpDownDB(f)
- yield db
- finally:
- f.close()
+ with _vti_updown_db_lock():
+ with open(VTI_WANT_UP_IFLIST, 'r+') as f:
+ yield VTIUpDownDB(f)
@contextmanager
def open_vti_updown_db_readonly():
- """ Opens the database for reading, returning an error if it does not exist """
- f = open(VTI_WANT_UP_IFLIST, 'r')
- try:
- db = VTIUpDownDB(f)
- yield db
- finally:
- f.close()
+ """Opens the database for reading. Yields None if the database does not exist."""
+ with _vti_updown_db_lock():
+ if not vti_updown_db_exists():
+ yield None
+ return
+ with open(VTI_WANT_UP_IFLIST, 'r') as f:
+ yield VTIUpDownDB(f)
def remove_vti_updown_db():
- """ Brings down any interfaces referenced by the database and removes the database """
- # We need to process the DB first to bring down any interfaces still up
- with open_vti_updown_db_for_update() as db:
- db.removeAllOtherInterfaces([])
- # this usage of commit will only ever bring down interfaces,
- # do not need to provide a functional interface dict supplier
- db.commit(lambda _: None)
-
- os.unlink(VTI_WANT_UP_IFLIST)
+ """Brings down any interfaces referenced by the database and removes the database, if it exists."""
+ with _vti_updown_db_lock():
+ if not vti_updown_db_exists():
+ return
+ # We hold the lock already; open the file directly rather than via the
+ # locking context manager to avoid re-acquiring (which would deadlock).
+ with open(VTI_WANT_UP_IFLIST, 'r+') as f:
+ db = VTIUpDownDB(f)
+ db.removeAllOtherInterfaces([])
+ # this usage of commit will only ever bring down interfaces,
+ # do not need to provide a functional interface dict supplier
+ db.commit(lambda _: None)
+ os.unlink(VTI_WANT_UP_IFLIST)
class VTIUpDownDB:
# The VTI Up-Down DB is a text-based database of space-separated "ifspecs".
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 78d5cbe83..be8eeca13 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -50,7 +50,6 @@ from vyos.utils.network import interface_exists
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.process import call
-from vyos.utils.vti_updown_db import vti_updown_db_exists
from vyos.utils.vti_updown_db import open_vti_updown_db_for_create_or_update
from vyos.utils.vti_updown_db import remove_vti_updown_db
from vyos import ConfigError
@@ -900,8 +899,7 @@ def apply(ipsec):
systemd_service = 'strongswan.service'
if not ipsec or 'deleted' in ipsec:
call(f'systemctl stop {systemd_service}')
- if vti_updown_db_exists():
- remove_vti_updown_db()
+ remove_vti_updown_db()
else:
call(f'systemctl reload-or-restart {systemd_service}')
if ipsec['enabled_vti_interfaces']:
@@ -909,7 +907,7 @@ def apply(ipsec):
db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
db.setPersistentInterfaces(ipsec['persistent_vti_interfaces'])
db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
- elif vti_updown_db_exists():
+ else:
remove_vti_updown_db()
if ipsec:
if ipsec.get('nhrp_exists', False):