Files
ubuntu-post-install/services/vpn-data-mount.sh
T
Claude 1a259e0895 Fix password prompt silently stripping leading/trailing whitespace
Reported live: mount error(79) again despite already switching to real
credentials + sec=ntlmssp — this time with a Samba password containing
special characters. Root cause confirmed directly: `read -r -s pw1`
without `IFS=` silently strips leading/trailing whitespace even when
reading into a single variable (verified: " P@ss word! " -> "P@ss word!",
10 chars instead of 12). A password with a leading/trailing space —
common from a password manager's copy-paste, or a stray keystroke — got
quietly trimmed on the way into the credentials file, so it no longer
matched what was actually set on the Samba account. That mismatch
surfaces as this same cryptic ENOKEY mount error, not an obvious "wrong
password".

Fixed with IFS= on both reads. Also echo the captured length (never the
password itself) right after entry, so a silently-stripped character is
something you can catch and cross-check yourself before the mount even
attempts, instead of only after it fails.
2026-08-10 19:35:57 +00:00

549 lines
24 KiB
Bash

#!/bin/bash
# services/vpn-data-mount.sh — mount existing SMB shares from a
# NetBird-connected home box, with SSH-key bootstrap automated.
# Part of the modular post-install system (sourced by setup.sh).
#
# Can also be run standalone on any machine:
# sudo bash vpn-data-mount.sh
# (No Docker needed — this only touches SSH and /etc/fstab)
#
# Unlike most services here, this is repeatable by design: different
# services can have data on different home boxes, so this asks for a home
# box IP every time and can be re-run any number of times, once per
# home-box you want to pull shares from. It's the multi-instance pattern
# from CLAUDE.md generalized from "N instances of one app" to "N
# independent mounts" — there's no single install directory to gate on, so
# state lives in /etc/fstab itself (tagged entries), same as
# tools/mount-network-drive.sh.
#
# Deliberately READ-ONLY on the home box's Samba config — this tool never
# writes to smb.conf, never installs Samba, never creates or resets a
# Samba account there. It only (a) bootstraps passwordless SSH if needed,
# then (b) reads the home box's existing smb.conf over that SSH connection
# to list whatever shares are already configured there, so you can pick
# one or more to mount. Set up the actual share(s) on the home box
# yourself, the normal way (or with tools/mount-network-drive.sh's own
# guided flow, run there). An earlier version of this tried to fully
# provision Samba remotely too — reversed per direct request, and it had
# also caused real damage in practice (a section-removal bug that deleted
# unrelated shares on a real box) that a read-only tool can't repeat.
#
# Assumes the home box is Linux and reachable over a NetBird IP — this repo
# doesn't set up the home box's side of NetBird (that's a separate machine,
# possibly not running this repo at all).
#
# SMB chosen over NFS/SSHFS deliberately: NFS is marginally faster for
# Linux-to-Linux but SMB isn't a "huge" difference for normal use (media,
# docs, moderate datasets — the gap shows up mainly on many-small-files
# workloads). SSHFS was ruled out because the VPN tunnel already encrypts
# everything — SSHFS's own SSH-layer encryption on top of that is pure
# redundant overhead for no added security, and it's the slowest and least
# robust (FUSE reconnect quirks) of the three for an always-on mount.
#
# Mounts use real Samba credentials (a username/password you provide for
# an account that already exists on the home box), stored locally in a
# root-only credentials file, same convention tools/mount-network-drive.sh
# already uses — never guest access. A CIFS guest mount with no explicit
# security mode can hit "mount error(79): Can not access a needed shared
# library" — a misleadingly-worded cifs-utils message for errno 79
# (ENOKEY), a known rough edge in the kernel cifs.ko keyring/upcall path
# for anonymous sessions. Credentialed mounts with an explicit sec=ntlmssp
# take the normal NTLMSSP auth path instead and don't hit it.
# ── Standalone bootstrap ──────────────────────────────────────────────────────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_COMMON="$_SELF_DIR/../lib/common.sh"
if [[ -f "$_COMMON" ]]; then
# shellcheck source=../lib/common.sh
source "$_COMMON"
else
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
prompt_text() {
local _q="$1" _def="$2" _var="$3" _r
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
read -r -p " $_q " _r
eval "$_var='${_r:-$_def}'"
}
prompt_yn() {
local _q="$1" _def="$2" _var="$3" _r
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
read -r -p " $_q " _r
eval "$_var='${_r:-$_def}'"
}
register_service() { :; }
fi
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
DRY_RUN="${DRY_RUN:-false}"
UNATTENDED="${UNATTENDED:-false}"
_RUN_STANDALONE=1
fi
# ─────────────────────────────────────────────────────────────────────────────
register_service vpn-data-mount homelab "Mount existing SMB shares from a NetBird-connected home box (read-only discovery, SSH-automated key setup)"
# ── fstab tagging — the durable record of what this tool has set up ────────
# Same philosophy as tools/mount-network-drive.sh: /etc/fstab is the single
# source of truth, no separate state file to drift out of sync with it.
_VDM_TAG_PREFIX="# vpn-data-mount:"
_vdm_list_existing() {
local entries
entries="$(grep "^${_VDM_TAG_PREFIX}" /etc/fstab 2>/dev/null || true)"
if [ -n "$entries" ]; then
echo ""
log_info "Already-configured VPN data mounts:"
echo "$entries" | sed "s|^${_VDM_TAG_PREFIX}| •|"
echo ""
fi
}
# ── Name a raw IP so it can be used everywhere instead of typing it again ──
# Deliberately /etc/hosts, not ~/.ssh/config: an SSH Host alias only helps
# the `ssh` command itself resolve a name — mount.cifs (and everything
# else) never consults ~/.ssh/config at all, so an alias alone wouldn't let
# the actual CIFS mount address use a name. /etc/hosts is the one mechanism
# that makes a name resolve for both, which is what "use a name instead of
# the IP" actually needs end to end.
# Sets RESOLVED_HOST (not local — same out-param convention as elsewhere).
_vdm_resolve_host() {
local input="$1"
RESOLVED_HOST="$input"
# Not a raw IP (already a name, whether from /etc/hosts, real DNS, or
# just typed that way) — nothing to do.
[[ "$input" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 0
# Already named from an earlier vpn-data-mount run against this same IP?
local existing
existing="$(awk -v ip="$input" '$1==ip && /# vpn-data-mount/ {print $2; exit}' /etc/hosts 2>/dev/null)"
if [ -n "$existing" ]; then
log_info "Already named '$existing' in /etc/hosts from an earlier mount — using that."
RESOLVED_HOST="$existing"
return 0
fi
local NAME=""
prompt_text " Name this home box (blank to keep using the IP):" "" NAME
[ -z "$NAME" ] && return 0
NAME="$(echo "$NAME" | tr -cs 'a-zA-Z0-9-' '-' | sed 's/^-*//;s/-*$//')"
[ -z "$NAME" ] && return 0
if grep -qE "^\S+[[:space:]]+${NAME}([[:space:]]|\$)" /etc/hosts 2>/dev/null; then
log_warning "'$NAME' is already used for a different address in /etc/hosts — keeping the IP instead."
return 0
fi
echo "${input} ${NAME} # vpn-data-mount" >> /etc/hosts
log_success "Added to /etc/hosts: $NAME -> $input (works for SSH, this mount, and anything else on this box)"
RESOLVED_HOST="$NAME"
}
# ── SSH trust: test first, only bootstrap if actually needed ──────────────
# Covers "the home box and VPS already share a key via GitHub import (or any
# other means)" for free — if it already works, nothing below runs at all.
_vdm_ssh_works() {
local user="$1" host="$2"
# Runs as $ACTUAL_USER, not root (this whole script runs as root) — the
# SSH key lives in $ACTUAL_HOME/.ssh, so root's own bare `ssh` would look
# in the wrong home directory entirely and never find it.
sudo -u "$ACTUAL_USER" ssh -o BatchMode=yes -o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new \
"${user}@${host}" true 2>/dev/null
}
_vdm_ensure_ssh_trust() {
local user="$1" host="$2"
if _vdm_ssh_works "$user" "$host"; then
log_success "Passwordless SSH to ${user}@${host} already works — nothing to set up."
return 0
fi
log_info "No passwordless SSH to ${user}@${host} yet — setting it up."
local keyfile="$ACTUAL_HOME/.ssh/id_ed25519"
if [ ! -f "$keyfile" ]; then
log_info "No SSH key found at $keyfile — generating one."
sudo -u "$ACTUAL_USER" mkdir -p "$ACTUAL_HOME/.ssh"
sudo -u "$ACTUAL_USER" ssh-keygen -t ed25519 -N "" -f "$keyfile" -C "${ACTUAL_USER}@$(hostname)-vpn-data-mount" \
|| { log_error "Key generation failed."; return 1; }
chmod 700 "$ACTUAL_HOME/.ssh"
chmod 600 "$keyfile"
chmod 644 "${keyfile}.pub"
fi
echo ""
echo " This box's public key (needs to end up in ${user}'s authorized_keys"
echo " on the home box, one way or another):"
echo ""
sed 's/^/ /' "${keyfile}.pub"
echo ""
while true; do
echo " How do you want to get it there?"
echo " 1) Try now with ssh-copy-id (needs password login enabled on the home box)"
echo " 2) I'll add it myself — paste it into ~/.ssh/authorized_keys there, or add it"
echo " to your GitHub account and run 'ssh-import-id gh:<user>' on the home box"
echo " (same mechanism this repo's own base.sh setup uses)"
echo " 3) Cancel this mount"
echo ""
local CHOICE=""
prompt_text " Choice [1/2/3]:" "1" CHOICE
case "$CHOICE" in
1)
sudo -u "$ACTUAL_USER" ssh-copy-id -i "${keyfile}.pub" "${user}@${host}" \
|| log_warning "ssh-copy-id failed — password auth may be disabled on the home box. Try option 2."
;;
2)
echo ""
read -r -p " Press Enter once the key is in place on the home box: " _
;;
3|c|C)
log_info "Cancelled."
return 1
;;
*)
log_warning "Invalid choice."
continue
;;
esac
if _vdm_ssh_works "$user" "$host"; then
log_success "Passwordless SSH to ${user}@${host} confirmed."
return 0
fi
log_warning "Still can't connect without a password — try again, or cancel."
done
}
# ── Read-only share discovery ───────────────────────────────────────────────
# Prints "share_name|path" one per line for every real data share found in
# the home box's smb.conf (skips [global]/[homes]/[printers]/[print$] —
# not actual browsable directories). Never writes anything, on either
# side — see the file header. Tries a plain read first (smb.conf is
# world-readable on a stock Samba install); falls back to a sudo'd read
# only if that comes back empty, still read-only either way.
_vdm_list_remote_shares() {
local user="$1" host="$2"
local conf
conf="$(sudo -u "$ACTUAL_USER" ssh "${user}@${host}" 'cat /etc/samba/smb.conf 2>/dev/null')"
if [ -z "$conf" ]; then
conf="$(sudo -u "$ACTUAL_USER" ssh -t "${user}@${host}" 'sudo cat /etc/samba/smb.conf 2>/dev/null' 2>/dev/null)"
fi
[ -z "$conf" ] && return 1
echo "$conf" | awk '
function flush() {
if (sect != "" && path != "" && sect != "global" && sect != "printers" && sect != "print$" && sect != "homes") {
print sect "|" path
}
}
/^\[/ {
flush()
sect = $0
gsub(/[][]/, "", sect)
path = ""
next
}
/^[[:space:]]*path[[:space:]]*=/ {
path = $0
sub(/^[[:space:]]*path[[:space:]]*=[[:space:]]*/, "", path)
gsub(/[[:space:]]+$/, "", path)
}
END { flush() }
'
}
# ── Reuse a password already entered for the same user+host ───────────────
# Prints the password if an earlier mount from this host used the same
# Samba username, nothing otherwise. No log_* calls in here — this runs
# inside a caller's $(...) capture, and log_info/log_warning/etc. all
# write to stdout, which would corrupt it.
_vdm_find_existing_smb_password() {
local host="$1" user="$2" label creds_file found_user found_pass
while IFS= read -r label; do
[ -z "$label" ] && continue
creds_file="/etc/samba/credentials.vpn-data-mount-${label}"
[ -f "$creds_file" ] || continue
found_user="$(grep '^username=' "$creds_file" | cut -d= -f2-)"
[ "$found_user" = "$user" ] || continue
found_pass="$(grep '^password=' "$creds_file" | cut -d= -f2-)"
[ -n "$found_pass" ] && { printf '%s' "$found_pass"; return 0; }
done < <(grep -E "^${_VDM_TAG_PREFIX} [^ ]+ — ${host}:" /etc/fstab 2>/dev/null \
| sed -E "s/^${_VDM_TAG_PREFIX} ([^ ]+) .*/\1/")
return 1
}
# ── Prompt for a password twice, hidden, matching ──────────────────────────
# Prints the password on success. No log_* calls — same reason as above;
# uses plain stderr output instead so it's visible without corrupting a
# caller's $(...) capture.
_vdm_prompt_password() {
local prompt="$1" pw1="" pw2=""
while true; do
# IFS= matters here, not just -s/-r: plain `read -r pw1` (no IFS=)
# silently strips leading/trailing whitespace even into a single
# variable — confirmed live, a password with a leading/trailing
# space (copy-pasted from a password manager, a stray keystroke)
# got quietly trimmed on the way in, so the credentials file ended
# up holding a DIFFERENT password than the one actually set on the
# Samba account. That surfaces as a cryptic mount failure, not an
# obvious "wrong password" — nothing here could tell the two apart.
echo -n " ${prompt}: " >&2
IFS= read -r -s pw1; echo "" >&2
echo -n " Confirm: " >&2
IFS= read -r -s pw2; echo "" >&2
if [ -n "$pw1" ] && [ "$pw1" = "$pw2" ]; then
# Length only, never the password itself — lets you catch a
# silently-stripped character (or a typo) yourself before the
# mount attempt fails with an unhelpful error.
echo " Captured (${#pw1} characters)." >&2
printf '%s' "$pw1"
return 0
fi
echo " Passwords didn't match or were empty — try again." >&2
done
}
# ── Local mount + fstab ─────────────────────────────────────────────────────
_vdm_mount_local() {
local host="$1" share_name="$2" mount_point="$3" label="$4" smb_user="$5" smb_pass="$6"
command -v mount.cifs >/dev/null 2>&1 || apt-get install -y cifs-utils -qq
mkdir -p "$mount_point"
# Credentials file, not guest/inline password — root-only, matching
# tools/mount-network-drive.sh's existing convention for SMB creds.
local creds_file="/etc/samba/credentials.vpn-data-mount-${label}"
mkdir -p /etc/samba
cat > "$creds_file" << CREDS
username=${smb_user}
password=${smb_pass}
CREDS
chmod 600 "$creds_file"
chown root:root "$creds_file"
# sec=ntlmssp explicitly — see the file header on errno 79/ENOKEY.
local opts="credentials=${creds_file},sec=ntlmssp,uid=$(id -u "$ACTUAL_USER"),gid=$(id -g "$ACTUAL_USER"),iocharset=utf8,nofail,_netdev"
local share="//${host}/${share_name}"
log_info "Testing mount..."
if mount -t cifs -o "$opts" "$share" "$mount_point"; then
log_success "Mounted at $mount_point"
else
log_error "Mount failed for [$share_name] — check the username/password and that the home box's share actually allows this account."
rmdir "$mount_point" 2>/dev/null || true
rm -f "$creds_file"
return 1
fi
if grep -qs "$mount_point" /etc/fstab; then
log_warning "$mount_point already in /etc/fstab — skipping fstab entry."
return 0
fi
local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
cp /etc/fstab "$bk"
{
echo ""
echo "${_VDM_TAG_PREFIX} ${label}${host}:${share_name} -> ${mount_point}"
printf '%-40s %-25s %-6s %s 0 0\n' "$share" "$mount_point" "cifs" "$opts"
} >> /etc/fstab
log_success "Added to /etc/fstab (backup: $(basename "$bk"))"
}
# ── Parse a selection like "1", "1,3", "1-3", "1 3 5" into 1-based indices ──
# Prints one index per line. Silently drops anything that doesn't look like
# a number or a range — the caller validates indices against the actual
# list length.
_vdm_parse_selection() {
local input="$1" token start end i
for token in $(echo "$input" | tr ',' ' '); do
if [[ "$token" =~ ^([0-9]+)-([0-9]+)$ ]]; then
start="${BASH_REMATCH[1]}"; end="${BASH_REMATCH[2]}"
for ((i = start; i <= end; i++)); do echo "$i"; done
elif [[ "$token" =~ ^[0-9]+$ ]]; then
echo "$token"
fi
done
}
# ── One home box, one or more shares from it ────────────────────────────────
_vdm_add_mount() {
echo ""
local HOST_INPUT="" HOST="" SSH_USER=""
prompt_text " Home box's NetBird IP or an already-named host (check 'netbird status' on that box):" "" HOST_INPUT
if [ -z "$HOST_INPUT" ]; then
log_warning "No host entered — cancelling."
return 1
fi
_vdm_resolve_host "$HOST_INPUT"
HOST="$RESOLVED_HOST"
prompt_text " SSH username on the home box:" "$ACTUAL_USER" SSH_USER
_vdm_ensure_ssh_trust "$SSH_USER" "$HOST" || return 1
# Pure convenience on top of the /etc/hosts naming above (which is what
# actually makes the mount itself usable by name) — an SSH Host alias
# additionally skips typing the username for interactive `ssh` use.
if [[ ! "$HOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] \
&& declare -F add_ssh_host_alias >/dev/null 2>&1 \
&& declare -F ssh_host_alias_exists >/dev/null 2>&1 \
&& ! ssh_host_alias_exists "$HOST"; then
local ADD_ALIAS=""
prompt_yn " Also add '$HOST' as an SSH alias (ssh $HOST, no username needed)? (y/n):" "y" ADD_ALIAS
[[ "$ADD_ALIAS" =~ ^[Yy]$ ]] && add_ssh_host_alias "$HOST" "$HOST_INPUT" "$SSH_USER" "22"
fi
log_info "Reading Samba shares already configured on $HOST (read-only)..."
local shares_raw
shares_raw="$(_vdm_list_remote_shares "$SSH_USER" "$HOST")"
if [ -z "$shares_raw" ]; then
log_warning "No Samba shares found on $HOST (or /etc/samba/smb.conf couldn't be read). Set up a share there first, the normal way, then re-run this."
return 1
fi
local share_names=() share_paths=()
while IFS='|' read -r sname spath; do
[ -z "$sname" ] && continue
share_names+=("$sname")
share_paths+=("$spath")
done <<< "$shares_raw"
echo ""
echo " Samba shares found on $HOST:"
local i
for i in "${!share_names[@]}"; do
printf " %d) %-20s %s\n" "$((i + 1))" "${share_names[$i]}" "${share_paths[$i]}"
done
echo ""
local SELECTION=""
prompt_text " Which one(s)? e.g. '1' or '1,3' or '1-3' or '1 3 5':" "" SELECTION
if [ -z "$SELECTION" ]; then
log_warning "Nothing selected — cancelling."
return 1
fi
local indices=()
while IFS= read -r i; do indices+=("$i"); done < <(_vdm_parse_selection "$SELECTION")
if [ "${#indices[@]}" -eq 0 ]; then
log_warning "Couldn't parse a selection from '$SELECTION' — cancelling."
return 1
fi
# Credentials asked once, reused for every share picked here — the
# common case is one personal account with access to several shares.
# Re-run this for a share needing a different account.
local SMB_USER=""
prompt_text " Samba username to connect with:" "$SSH_USER" SMB_USER
local SMB_PASS=""
SMB_PASS="$(_vdm_find_existing_smb_password "$HOST" "$SMB_USER")"
if [ -n "$SMB_PASS" ]; then
log_info "Reusing the Samba password already saved for '$SMB_USER' on $HOST from an earlier mount."
else
SMB_PASS="$(_vdm_prompt_password "Samba password for '$SMB_USER'")"
fi
local picked_any=false idx arr_i this_share this_path LABEL MOUNT_POINT
for idx in "${indices[@]}"; do
arr_i=$((idx - 1))
if [ "$arr_i" -lt 0 ] || [ "$arr_i" -ge "${#share_names[@]}" ]; then
log_warning "$idx isn't one of the listed shares — skipping."
continue
fi
this_share="${share_names[$arr_i]}"
this_path="${share_paths[$arr_i]}"
echo ""
log_info "Setting up: [$this_share] -> $this_path"
LABEL=""
while true; do
prompt_text " Local label for this mount:" "$this_share" LABEL
LABEL="$(echo "$LABEL" | tr -cs 'a-zA-Z0-9-' '-' | sed 's/^-*//;s/-*$//')"
if [ -z "$LABEL" ]; then
log_warning "Label can't be empty."; continue
fi
if grep -q "^${_VDM_TAG_PREFIX} ${LABEL} " /etc/fstab 2>/dev/null; then
log_warning "Label '$LABEL' is already used — pick another."; continue
fi
break
done
MOUNT_POINT=""
prompt_text " Local mount point:" "/mnt/${LABEL}" MOUNT_POINT
if _vdm_mount_local "$HOST" "$this_share" "$MOUNT_POINT" "$LABEL" "$SMB_USER" "$SMB_PASS"; then
# Read by callers like services/filebrowser.sh/audiobookshelf.sh/
# emby.sh that chain into this service and want to default
# their own "which directory" prompt to whatever was just
# mounted. Last one wins if several were picked in this run.
VDM_LAST_MOUNT_POINT="$MOUNT_POINT"
picked_any=true
fi
done
if [ "$picked_any" = true ]; then
echo ""
log_success "Done."
echo " Manage this and other network mounts anytime with:"
echo " sudo bash tools/mount-network-drive.sh"
fi
}
install_vpn-data-mount() {
echo ""
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ VPN Data Mount — mount existing SMB shares from a home ║"
echo "║ box over NetBird (read-only — nothing changes there) ║"
echo "╚══════════════════════════════════════════════════════════╝"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would offer to name a raw IP in /etc/hosts for reuse (SSH + this mount)"
echo "[DRY-RUN] Would test/set up passwordless SSH to a home box over its NetBird IP"
echo "[DRY-RUN] Would read-only list the home box's existing Samba shares (never writes there)"
echo "[DRY-RUN] Would let you pick one or more by number and mount them locally over CIFS"
echo "[DRY-RUN] Would add each to /etc/fstab with a root-only credentials file (not guest)"
echo "[DRY-RUN] Repeatable — can be run again for additional home boxes"
return 0
fi
# Every prompt below (home box IP, share selection, ...) has no sane
# unattended default — unlike most services here, there's no reasonable
# value to fall back to. Skip outright rather than let prompt_text's
# always-blank UNATTENDED behavior spin something forever.
if [ "$UNATTENDED" = true ]; then
log_info "Skipping — needs interactive input (home box IP, share selection, ...). Run 'sudo ./setup.sh vpn-data-mount' without --unattended."
return 0
fi
_vdm_list_existing
while true; do
local ADD=""
prompt_yn "Connect to a home box and mount some of its shares now? (y/n):" "y" ADD
[[ "$ADD" =~ ^[Yy]$ ]] || break
_vdm_add_mount
local AGAIN=""
prompt_yn "Connect to another (different) home box? (y/n):" "n" AGAIN
[[ "$AGAIN" =~ ^[Yy]$ ]] || break
done
}
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_vpn-data-mount