@@ -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+
69176928function 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 .
69846995function 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+
70367065function 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)
72667301if [[ " ${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
0 commit comments