Skip to content

Commit 779e759

Browse files
committed
fix: serialize protocol and client mutations
1 parent 0035513 commit 779e759

3 files changed

Lines changed: 77 additions & 14 deletions

File tree

amneziawg-install.sh

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6914,6 +6914,17 @@ function changeAwgProtocolInteractively() {
69146914
setAwgProtocolMode "${TARGET_MODE}"
69156915
}
69166916

6917+
# Interactive management can remain open while the web panel or another CLI
6918+
# changes protocol state. Reload persisted params only after taking the shared
6919+
# lifecycle lock, and keep the lock through the complete mutation so a staged
6920+
# protocol transaction can never overwrite the result.
6921+
function runLockedManagementOperation() (
6922+
local OPERATION="$1"
6923+
acquireClientLifecycleLock || return 1
6924+
loadParams
6925+
"${OPERATION}"
6926+
)
6927+
69176928
function manageMenu() {
69186929
local MENU_OPTION=""
69196930
echo "AmneziaWG server installer (https://github.com/wiresock/amneziawg-install)"
@@ -6937,22 +6948,22 @@ function manageMenu() {
69376948
done
69386949
case "${MENU_OPTION}" in
69396950
1)
6940-
newClient
6951+
runLockedManagementOperation newClient
69416952
;;
69426953
2)
69436954
listClients
69446955
;;
69456956
3)
6946-
revokeClient
6957+
runLockedManagementOperation revokeClient
69476958
;;
69486959
4)
6949-
regenerateClients
6960+
runLockedManagementOperation regenerateClients
69506961
;;
69516962
5)
69526963
changeAwgProtocolInteractively
69536964
;;
69546965
6)
6955-
uninstallAmneziaWG
6966+
runLockedManagementOperation uninstallAmneziaWG
69566967
;;
69576968
7)
69586969
exit 0
@@ -6979,8 +6990,8 @@ CLIENT_LIFECYCLE_LOCK_FD=""
69796990
# persistent state directory is opened read-only, its descriptor identity
69806991
# is revalidated against the path, and the descriptor is locked. The root-run
69816992
# CLI therefore never creates, truncates, chowns, or chmods a service-writable
6982-
# lock pathname. The caller runs in a subshell so the descriptor, and therefore
6983-
# the lock, is released on every success, return, or exit path.
6993+
# lock pathname. Mutating callers run in a subshell so the descriptor is closed
6994+
# automatically; the interactive menu preloader releases it explicitly.
69846995
function acquireClientLifecycleLock() {
69856996
local lock_dir env_file panel_installed=0
69866997
local old_umask dir_identity descriptor_identity descriptor_path
@@ -7028,11 +7039,29 @@ function acquireClientLifecycleLock() {
70287039
fi
70297040
if ! flock -xn "${CLIENT_LIFECYCLE_LOCK_FD}"; then
70307041
exec {CLIENT_LIFECYCLE_LOCK_FD}>&-
7031-
echo "ERROR: another add/remove operation is already in progress" >&2
7042+
echo "ERROR: another client or protocol management operation is already in progress" >&2
70327043
return 1
70337044
fi
70347045
}
70357046

7047+
function releaseClientLifecycleLock() {
7048+
if [[ -n "${CLIENT_LIFECYCLE_LOCK_FD:-}" ]]; then
7049+
exec {CLIENT_LIFECYCLE_LOCK_FD}>&- 2>/dev/null || true
7050+
CLIENT_LIFECYCLE_LOCK_FD=""
7051+
fi
7052+
}
7053+
7054+
# The initial interactive menu needs loaded values for display, but must not
7055+
# hold the lifecycle lock while waiting for input. Mutating menu actions reload
7056+
# again under their own lock through runLockedManagementOperation.
7057+
function loadParamsForManagementMenu() {
7058+
local RC=0
7059+
acquireClientLifecycleLock || return 1
7060+
loadParams || RC=$?
7061+
releaseClientLifecycleLock
7062+
return "${RC}"
7063+
}
7064+
70367065
function nonInteractiveAddClient() (
70377066
local CLIENT_NAME="$1"
70387067

@@ -7050,8 +7079,11 @@ function nonInteractiveAddClient() (
70507079
exit 1
70517080
fi
70527081
acquireClientLifecycleLock || exit 1
7082+
# Reload only after locking: a protocol transaction may have completed after
7083+
# this CLI process started but before it acquired the lifecycle lock.
7084+
loadParams
70537085

7054-
# Ensure params are loaded and config path is set
7086+
# Ensure the config path follows the freshly loaded interface name.
70557087
SERVER_AWG_CONF="${AMNEZIAWG_DIR}/${SERVER_AWG_NIC}.conf"
70567088

70577089
# Check for duplicate name
@@ -7223,6 +7255,7 @@ function nonInteractiveRemoveClient() (
72237255
exit 1
72247256
fi
72257257
acquireClientLifecycleLock || exit 1
7258+
loadParams
72267259

72277260
SERVER_AWG_CONF="${AMNEZIAWG_DIR}/${SERVER_AWG_NIC}.conf"
72287261

@@ -7257,10 +7290,12 @@ function nonInteractiveRemoveClient() (
72577290
echo "OK"
72587291
)
72597292

7260-
function nonInteractiveListClients() {
7293+
function nonInteractiveListClients() (
7294+
acquireClientLifecycleLock || exit 1
7295+
loadParams
72617296
SERVER_AWG_CONF="${AMNEZIAWG_DIR}/${SERVER_AWG_NIC}.conf"
72627297
grep -E "^### Client" "${SERVER_AWG_CONF}" | cut -d ' ' -f 3 || true
7263-
}
7298+
)
72647299

72657300
# Only run main logic when executed directly (not when sourced for testing)
72667301
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
@@ -7311,7 +7346,6 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
73117346
echo "ERROR: AmneziaWG is not installed (params file missing)" >&2
73127347
exit 1
73137348
fi
7314-
loadParams
73157349
nonInteractiveAddClient "$2"
73167350
exit $?
73177351
;;
@@ -7325,7 +7359,6 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
73257359
echo "ERROR: AmneziaWG is not installed (params file missing)" >&2
73267360
exit 1
73277361
fi
7328-
loadParams
73297362
nonInteractiveRemoveClient "$2"
73307363
exit $?
73317364
;;
@@ -7335,7 +7368,6 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
73357368
echo "ERROR: AmneziaWG is not installed (params file missing)" >&2
73367369
exit 1
73377370
fi
7338-
loadParams
73397371
nonInteractiveListClients
73407372
exit $?
73417373
;;
@@ -7350,7 +7382,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
73507382
if [[ "${OS}" == "ubuntu" ]]; then
73517383
refreshConfiguredUbuntuAmneziaPpa
73527384
fi
7353-
loadParams
7385+
loadParamsForManagementMenu || exit 1
73547386
manageMenu
73557387
else
73567388
installAmneziaWG

tests/test-awg3.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,36 @@ else
294294
not_ok "protocol changes lock and reload persisted state before deciding a toggle is a no-op"
295295
fi
296296

297+
MANAGEMENT_LOCK_ORDER="${TEST_ROOT}/management-lock-order.log"
298+
: >"${MANAGEMENT_LOCK_ORDER}"
299+
if (
300+
AWG_PROTOCOL_VERSION=2
301+
acquireClientLifecycleLock() { printf 'lock\n' >>"${MANAGEMENT_LOCK_ORDER}"; }
302+
loadParams() {
303+
printf 'load\n' >>"${MANAGEMENT_LOCK_ORDER}"
304+
AWG_PROTOCOL_VERSION=3
305+
}
306+
_testManagementMutation() { printf 'mutate-%s\n' "${AWG_PROTOCOL_VERSION}" >>"${MANAGEMENT_LOCK_ORDER}"; }
307+
runLockedManagementOperation _testManagementMutation
308+
) && [[ "$(paste -sd, "${MANAGEMENT_LOCK_ORDER}")" == "lock,load,mutate-3" ]]; then
309+
ok "interactive mutations lock and reload current protocol state before writing"
310+
else
311+
not_ok "interactive mutations lock and reload current protocol state before writing"
312+
fi
313+
314+
MENU_LOAD_LOCK_ORDER="${TEST_ROOT}/menu-load-lock-order.log"
315+
: >"${MENU_LOAD_LOCK_ORDER}"
316+
if (
317+
acquireClientLifecycleLock() { printf 'lock\n' >>"${MENU_LOAD_LOCK_ORDER}"; }
318+
loadParams() { printf 'load\n' >>"${MENU_LOAD_LOCK_ORDER}"; }
319+
releaseClientLifecycleLock() { printf 'release\n' >>"${MENU_LOAD_LOCK_ORDER}"; }
320+
loadParamsForManagementMenu
321+
) && [[ "$(paste -sd, "${MENU_LOAD_LOCK_ORDER}")" == "lock,load,release" ]]; then
322+
ok "interactive menu preload releases the lifecycle lock before waiting for input"
323+
else
324+
not_ok "interactive menu preload releases the lifecycle lock before waiting for input"
325+
fi
326+
297327
SAME_MODE_REPAIR_LOG="${TEST_ROOT}/same-mode-repair.log"
298328
: >"${SAME_MODE_REPAIR_LOG}"
299329
if (

tests/test-functions.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,7 @@ _run_niac() { # $1=ENABLE_IPV6 (y/n) $2=client name
13221322
SERVER_AWG_H1="5-10"; SERVER_AWG_H2="11-20"; SERVER_AWG_H3="21-30"; SERVER_AWG_H4="31-40"
13231323
ALLOWED_IPS="0.0.0.0/0,::/0"; ENABLE_IPV6="$1"
13241324
# Neutralize side-effecting helpers for the unit test.
1325+
loadParams() { :; }
13251326
ensureAmneziawgKernelModule() { :; }
13261327
copyToWebPanelDir() { :; }
13271328
nonInteractiveAddClient "$2" >/dev/null 2>&1

0 commit comments

Comments
 (0)