From dd6f1d0a5d7e82c8e28e0b247ff046364998b44a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 19:24:59 +0000 Subject: [PATCH] Make vpn-data-mount strictly read-only on the remote Samba config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per direct request: never write to the home box's smb.conf at all, not even carefully — just discover what's already shared there and mount it. Removes all remote provisioning (installing Samba, creating/removing share blocks, resetting smbpasswd accounts) entirely, which also removes the whole class of bug the previous two fixes were patching around (destructive section-removal, clobbering another mount's saved password) — a tool that can't write can't repeat that kind of damage. New flow: resolve/name the host and bootstrap SSH trust as before, then read-only list every real share already in the home box's smb.conf (skipping [global]/[homes]/[printers]/[print$]) via a plain SSH `cat`, falling back to a sudo'd read only if that comes back empty — still only ever reading. Presents them as a numbered list and accepts a flexible selection ('1', '1,3', '1-3', '1 3 5', or combinations), asks once for the Samba username/password to connect with (reusing a previously-saved password for the same user+host if one exists), then mounts each picked share locally over CIFS with its own /etc/fstab entry — same as before. Verified the selection parser against all the documented formats plus a mixed comma+range case and garbage/empty input. --- README.md | 2 +- services/vpn-data-mount.sh | 420 +++++++++++++++++-------------------- 2 files changed, 193 insertions(+), 229 deletions(-) diff --git a/README.md b/README.md index 91d33b6..b8a39f2 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ a ready-to-copy Caddy config snippet to `~/docker/caddy-snippets/`. | Group | Services | |-------|---------| | `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network | -| `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine`, `vpn-data-mount` (SMB mount from a NetBird-connected home box — SSH trust bootstrap + remote Samba setup automated over SSH; repeatable, one home box/share at a time) | +| `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine`, `vpn-data-mount` (mount existing SMB shares from a NetBird-connected home box — SSH trust bootstrap, then read-only discovery of shares already configured there; never writes to the home box's Samba config; repeatable, pick from any number of a home box's shares in one pass) | | `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy`, `wordpress` (multi-site, dedicated MariaDB per site — blogs, business sites, e-commerce via WooCommerce) | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | diff --git a/services/vpn-data-mount.sh b/services/vpn-data-mount.sh index 5aefbaf..e10b526 100644 --- a/services/vpn-data-mount.sh +++ b/services/vpn-data-mount.sh @@ -1,25 +1,36 @@ #!/bin/bash -# services/vpn-data-mount.sh — mount SMB data from a NetBird-connected home -# box, with SSH-key bootstrap and remote Samba setup automated over SSH. +# 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, Samba, and /etc/fstab) +# (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/share you want mounted. 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. +# 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); it only automates the VPS side: -# SSH trust, then using that SSH access to configure Samba on the home box -# remotely, then mounting it here. +# 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, @@ -29,20 +40,15 @@ # 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. # -# Each share gets a dedicated Samba account (same username as the SSH user, -# since that Unix account already exists on the home box — no extra remote -# user to create) with its own generated password, stored locally in a +# 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. An earlier version of this made the share guest-accessible -# instead, reasoning the VPN itself was enough access control — reversed -# per direct request (real per-share accounts, not root/guest) and because -# it incidentally fixes a real bug: plain `guest` CIFS mounts with no -# explicit `sec=` can hit "mount error(79): Can not access a needed shared +# 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 specifically, not an actual missing library. -# Credentialed mounts with an explicit sec= take the normal NTLMSSP auth -# path instead and don't hit it. +# 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 @@ -86,7 +92,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then fi # ───────────────────────────────────────────────────────────────────────────── -register_service vpn-data-mount homelab "Mount SMB data from a NetBird-connected home box (SSH-automated remote setup)" +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 @@ -113,7 +119,7 @@ _vdm_list_existing() { # the IP" actually needs end to end. # Sets RESOLVED_HOST (not local — same out-param convention as elsewhere). _vdm_resolve_host() { - local input="$1" default_name="$2" + local input="$1" RESOLVED_HOST="$input" # Not a raw IP (already a name, whether from /etc/hosts, real DNS, or @@ -129,14 +135,9 @@ _vdm_resolve_host() { return 0 fi - # prompt_text always falls back to the given default on blank input - # (same convention every other prompt in this repo relies on) — so this - # always ends up naming the host to something, never truly "blank" for - # "keep using the IP". That's fine: $default_name is already a sensible - # suggestion (the mount's own label), so Enter alone gives a reasonable - # name rather than requiring the extra step of confirming one. local NAME="" - prompt_text " Name for this home box (used instead of the IP from now on):" "$default_name" 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 @@ -227,31 +228,49 @@ _vdm_ensure_ssh_trust() { done } -# ── Detect an existing share exporting this exact path already ──────────── -# Prints the share name (bare, no brackets) if found, 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_remote_share() { - local user="$1" host="$2" remote_path="$3" - local awk_prog=' - /^\[/ { cur = $0; gsub(/[][]/, "", cur) } - /^[[:space:]]*path[[:space:]]*=/ { - val = $0 - sub(/^[[:space:]]*path[[:space:]]*=[[:space:]]*/, "", val) - gsub(/[[:space:]]+$/, "", val) - if (val == target) { print cur; exit } +# ── 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() } ' - sudo -u "$ACTUAL_USER" ssh "${user}@${host}" \ - "awk -v target='${remote_path}' '${awk_prog}' /etc/samba/smb.conf 2>/dev/null" } -# ── Reuse a password this tool already set for the same user+host ───────── -# A Samba account's password is shared across every share it can access — -# resetting it for a second mount from the same home box would silently -# break the first mount's already-saved credentials file. Prints the -# password if a prior mount from this host used the same Samba username, -# nothing otherwise. No log_* calls — same reason as above. +# ── 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 @@ -286,133 +305,6 @@ _vdm_prompt_password() { done } -# ── Remote Samba setup, driven entirely over the SSH trust above ────────── -# Sets SMB_PASS/SMB_USER/SMB_SHARE_NAME (not local — read by the caller), -# same out-param convention as lib/common.sh's -# ensure_coturn_user/configure_caddy_for_service. -_vdm_setup_remote_samba() { - local user="$1" host="$2" remote_path="$3" share_name="$4" - SMB_PASS="" SMB_USER="$user" SMB_SHARE_NAME="$share_name" - - log_info "Checking Samba on the home box..." - # Every ssh call below runs as $ACTUAL_USER, same reason as _vdm_ssh_works. - if ! sudo -u "$ACTUAL_USER" ssh "${user}@${host}" 'command -v smbd >/dev/null 2>&1'; then - log_info "Installing Samba on the home box (may prompt for the sudo password there)..." - sudo -u "$ACTUAL_USER" ssh -t "${user}@${host}" 'sudo apt-get update -y && sudo apt-get install -y samba' \ - || { log_error "Remote Samba install failed."; return 1; } - else - log_success "Samba already installed on the home box." - fi - - # Don't blindly overwrite a share that's already exporting this exact - # path — the home box may already have this configured by hand. - local existing_share - existing_share="$(_vdm_find_remote_share "$user" "$host" "$remote_path")" - if [ -n "$existing_share" ]; then - log_info "Found an existing Samba share '[$existing_share]' already exporting $remote_path on the home box." - local REUSE="" - prompt_yn " Use it as-is instead of creating a new one? (y/n):" "y" REUSE - if [[ "$REUSE" =~ ^[Yy]$ ]]; then - SMB_SHARE_NAME="$existing_share" - prompt_text " Samba username for that share:" "$user" SMB_USER - SMB_PASS="$(_vdm_prompt_password "Samba password for '$SMB_USER'")" - log_success "Reusing existing share [$SMB_SHARE_NAME] — nothing changed on the home box." - return 0 - fi - log_info "Creating a separate new share instead." - fi - - # Reuse this account's password if another mount from the same home - # box already set one up (see _vdm_find_existing_smb_password), rather - # than resetting an account that other saved credentials still depend on. - local reset_needed=true - SMB_PASS="$(_vdm_find_existing_smb_password "$host" "$user")" - if [ -n "$SMB_PASS" ]; then - log_info "Reusing the Samba password already set up for '$user' on $host (from another mount) — not resetting it." - reset_needed=false - elif sudo -u "$ACTUAL_USER" ssh "${user}@${host}" "sudo pdbedit -L 2>/dev/null | grep -q '^${user}:'"; then - log_warning "Samba account '$user' already exists on the home box with a password this tool doesn't know." - local RESET_PW="" - prompt_yn " Reset it to a newly generated password? (Anything already using the old one — other mounts, manual clients — will need updating.) (y/n):" "n" RESET_PW - if [[ "$RESET_PW" =~ ^[Yy]$ ]]; then - SMB_PASS="$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16)" - else - SMB_PASS="$(_vdm_prompt_password "Existing Samba password for '$user'")" - reset_needed=false - fi - else - SMB_PASS="$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16)" - fi - - log_info "Configuring the share on the home box..." - # Idempotent: drop any prior block for this exact share name, then - # append a fresh one. Dedicated account (not guest) — see the file - # header for why, and for the errno-79 connection. - # - # The Samba account reuses $user — that Unix account already exists on - # the home box (it's who we're SSH'd in as), so no extra remote user - # needs creating. When reset_needed, the new password ends up briefly - # visible in the home box's own `ps` output for this one remote - # command's duration (embedded in the command string, not piped — - # piping through a `-t` pty session for a command that may also need - # an interactive sudo password gets stdin-ordering-fragile fast). - # Accepted tradeoff, same spirit as this whole feature already trusting - # the VPN's reachability boundary — see file header. - local pw_cmd="" - if [ "$reset_needed" = true ]; then - pw_cmd="printf '%s\n%s\n' '${SMB_PASS}' '${SMB_PASS}' | sudo smbpasswd -a -s '${user}' -sudo smbpasswd -e '${user}'" - fi - - # Builds the new config in a scratch file and validates it with - # testparm BEFORE it ever touches the live smb.conf — confirmed live: - # the previous approach here (sed range-deleting from the [share_name] - # header through the next BLANK line) silently deleted straight through - # to end of file on a home box whose smb.conf had no blank line - # separating sections, taking unrelated shares down with it. Section - # removal now stops at the next `[section]` header (or EOF) instead of - # a blank line, which is the actual boundary of an INI-style section - # regardless of how the file happens to be formatted. - local remote_cmd - remote_cmd=$(cat << REMOTECMD -set -e -sudo mkdir -p '${remote_path}' -BACKUP="/etc/samba/smb.conf.backup.\$(date +%Y%m%d-%H%M%S)" -sudo cp /etc/samba/smb.conf "\$BACKUP" -sudo awk -v target='[${share_name}]' ' - /^\\[/ { skip = (\$0 == target) } - !skip { print } -' /etc/samba/smb.conf > /tmp/smb.conf.vdm.new -{ - echo "" - echo "[${share_name}]" - echo " path = ${remote_path}" - echo " browseable = yes" - echo " read only = no" - echo " guest ok = no" - echo " valid users = ${user}" - echo " force user = ${user}" -} >> /tmp/smb.conf.vdm.new -if sudo testparm -s /tmp/smb.conf.vdm.new >/dev/null 2>&1; then - sudo cp /tmp/smb.conf.vdm.new /etc/samba/smb.conf - rm -f /tmp/smb.conf.vdm.new -else - echo "New smb.conf failed testparm validation — leaving the existing config untouched. Backup at \$BACKUP, rejected draft at /tmp/smb.conf.vdm.new for inspection." >&2 - exit 1 -fi -${pw_cmd} -sudo systemctl restart smbd -command -v ufw >/dev/null 2>&1 && sudo ufw allow samba >/dev/null 2>&1 || true -REMOTECMD -) - if sudo -u "$ACTUAL_USER" ssh -t "${user}@${host}" "$remote_cmd"; then - log_success "Remote share [$share_name] -> $remote_path configured, smbd restarted." - else - log_error "Remote Samba configuration failed — check the output above." - return 1 - fi -} - # ── Local mount + fstab ───────────────────────────────────────────────────── _vdm_mount_local() { local host="$1" share_name="$2" mount_point="$3" label="$4" smb_user="$5" smb_pass="$6" @@ -421,7 +313,7 @@ _vdm_mount_local() { mkdir -p "$mount_point" - # Credentials file, not a guest/inline password — root-only, matching + # 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 @@ -433,7 +325,6 @@ CREDS chown root:root "$creds_file" # sec=ntlmssp explicitly — see the file header on errno 79/ENOKEY. - # Modern default and what a credentialed mount should use anyway. 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}" @@ -441,8 +332,9 @@ CREDS if mount -t cifs -o "$opts" "$share" "$mount_point"; then log_success "Mounted at $mount_point" else - log_error "Mount failed — check connectivity to $host and the remote share config." + 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 @@ -460,29 +352,32 @@ CREDS log_success "Added to /etc/fstab (backup: $(basename "$bk"))" } -# ── One mount, start to finish ────────────────────────────────────────────── +# ── 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 LABEL="" - while true; do - prompt_text " Short label for this mount (e.g. 'media', 'nas-docs'):" "" 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 - 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 this mount." + log_warning "No host entered — cancelling." return 1 fi - _vdm_resolve_host "$HOST_INPUT" "$LABEL" + _vdm_resolve_host "$HOST_INPUT" HOST="$RESOLVED_HOST" prompt_text " SSH username on the home box:" "$ACTUAL_USER" SSH_USER @@ -492,8 +387,6 @@ _vdm_add_mount() { # 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. - # Only offered for a genuinely new name, not every time this mount (or - # another one on the same host) is set up. 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 \ @@ -503,54 +396,125 @@ _vdm_add_mount() { [[ "$ADD_ALIAS" =~ ^[Yy]$ ]] && add_ssh_host_alias "$HOST" "$HOST_INPUT" "$SSH_USER" "22" fi - local REMOTE_PATH="" MOUNT_POINT="" - prompt_text " Path on the home box to share (e.g. /home/${SSH_USER}/media):" "" REMOTE_PATH - if [ -z "$REMOTE_PATH" ]; then - log_warning "No path entered — cancelling this mount." + 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 - prompt_text " Local mount point:" "/mnt/${LABEL}" MOUNT_POINT - # SMB_USER/SMB_SHARE_NAME may differ from SSH_USER/LABEL if an existing - # remote share for this exact path was found and reused as-is. - _vdm_setup_remote_samba "$SSH_USER" "$HOST" "$REMOTE_PATH" "$LABEL" || return 1 - _vdm_mount_local "$HOST" "$SMB_SHARE_NAME" "$MOUNT_POINT" "$LABEL" "$SMB_USER" "$SMB_PASS" || return 1 - - # 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, without needing to - # know or guess the path themselves. - VDM_LAST_MOUNT_POINT="$MOUNT_POINT" + 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 "" - log_success "Done: $HOST:$REMOTE_PATH is now mounted at $MOUNT_POINT" - echo " Manage this and other network mounts anytime with:" - echo " sudo bash tools/mount-network-drive.sh" + 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 — SMB share from a NetBird-connected box ║" + 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 check for an existing Samba share/account first and offer to reuse it, not overwrite it" - echo "[DRY-RUN] Otherwise would remotely install Samba + configure a new share and dedicated account" - echo "[DRY-RUN] Would mount it locally over CIFS (credentials file, not guest) and add it to /etc/fstab" - echo "[DRY-RUN] Repeatable — can be run again for additional home boxes/shares" + 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, remote path, ...) has no sane + # 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 the label-validation loop below - # forever. + # always-blank UNATTENDED behavior spin something forever. if [ "$UNATTENDED" = true ]; then - log_info "Skipping — needs interactive input (home box IP, path, ...). Run 'sudo ./setup.sh vpn-data-mount' without --unattended." + log_info "Skipping — needs interactive input (home box IP, share selection, ...). Run 'sudo ./setup.sh vpn-data-mount' without --unattended." return 0 fi @@ -558,13 +522,13 @@ install_vpn-data-mount() { while true; do local ADD="" - prompt_yn "Add a VPN data mount now? (y/n):" "y" 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 "Add another mount (can be from a different home box)? (y/n):" "n" AGAIN + prompt_yn "Connect to another (different) home box? (y/n):" "n" AGAIN [[ "$AGAIN" =~ ^[Yy]$ ]] || break done }