diff options
| author | Robert Navarro <crshman@gmail.com> | 2026-06-18 19:13:04 -0700 |
|---|---|---|
| committer | Robert Navarro <crshman@gmail.com> | 2026-06-23 21:49:41 -0700 |
| commit | 368b0468bf4f1eaac8e0b61d0790206048eeb79a (patch) | |
| tree | c202480d379046f19adb6736d6791f182295d3e7 /python | |
| parent | e0114625c08a2a9826822958f53724995580716d (diff) | |
| download | vyos-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.
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/ifconfig/vti.py | 8 | ||||
| -rw-r--r-- | python/vyos/utils/vti_updown_db.py | 74 |
2 files changed, 47 insertions, 35 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". |
