Introduce `backtunnel-umount` as a portable unmount helper, preferring `fusermount3`, `fusermount`, or `umount`. Add `BACKTUNNEL_HOSTKEY_POLICY` for configurable host key handling in `backtunnel-share` and `backtunnel-access`. Update TUIs for remote folder prompts and mount point handling. Enhance bash completion for TUI commands with directory suggestions. Revamp terminal selection logic in `backtunnel-open-term` to prioritize modern emulators like wezterm. Extend tests with scaffolds for host key policy and unmount behavior. Update README with new scripts, workflows, features, and troubleshooting tips.
201 lines
6.8 KiB
Bash
201 lines
6.8 KiB
Bash
#!/usr/bin/env bash
|
||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
# Copyright (c) 2025 LUXIM d.o.o., Slovenia
|
||
# Author: Matjaž Mozetič
|
||
#
|
||
# Name: backtunnel-access
|
||
# Summary: Mount a local folder exposed via a reverse SSH tunnel (SFTP over sshfs).
|
||
# Description:
|
||
# Connects to a reverse SSH listener on the remote host's loopback (localhost:PORT),
|
||
# and mounts the specified local folder from the sharing side using sshfs.
|
||
# Performs basic checks (mountpoint readiness, auth hint, SFTP visibility) and
|
||
# sets sensible reconnect/keepalive options for a stable mount.
|
||
#
|
||
# Usage:
|
||
# backtunnel-access /path/to/folder from remoteuser:remotehost [-p PORT] [-m MOUNTPOINT]
|
||
#
|
||
# Examples:
|
||
# backtunnel-access ~/projects from alice@vps.example.com -p 2222 -m ~/remote-rssh
|
||
# backtunnel-access /data from bob:vps.example.com --port 4422 --mount-point /mnt/remote-rssh
|
||
#
|
||
# Dependencies:
|
||
# - bash
|
||
# - sshfs, sftp (OpenSSH), mountpoint
|
||
#
|
||
# Exit codes:
|
||
# 0 success
|
||
# 1 invalid usage/arguments or validation failure (e.g., mountpoint not writable)
|
||
# 2+ runtime errors from underlying tools (sshfs/sftp/etc.)
|
||
#
|
||
# Notes:
|
||
# - Expects a reverse tunnel to be active on the remote side, binding localhost:PORT
|
||
# back to the sharer’s local sshd.
|
||
# - If passwordless auth isn’t set for $REMOTE_USER@localhost:$PORT, a one-time
|
||
# setup command is suggested; mount may still proceed with password prompts.
|
||
|
||
# backtunnel-access: Mount a folder shared over reverse SSH
|
||
# Usage: backtunnel-access /path/to/folder from remoteuser:remotehost [-p PORT] [-m MOUNTPOINT]
|
||
|
||
set -euo pipefail
|
||
|
||
PORT=2222
|
||
MOUNTPOINT="$HOME/remote-rssh"
|
||
|
||
# Host key checking policy: env BACKTUNNEL_HOSTKEY_POLICY = yes|no|ask|accept-new (default: accept-new)
|
||
HKP="${BACKTUNNEL_HOSTKEY_POLICY:-accept-new}"
|
||
case "$HKP" in
|
||
yes|no|ask|accept-new) ;;
|
||
*) HKP="accept-new" ;;
|
||
esac
|
||
|
||
usage() {
|
||
echo "Usage: $0 /path/to/folder from remoteuser:remotehost [-p PORT] [-m MOUNTPOINT]" >&2
|
||
exit 1
|
||
}
|
||
|
||
# --- parse positional args ---
|
||
# Purpose: enforce grammar "<folder> from <remote>" and collect required args.
|
||
[[ $# -lt 3 ]] && usage
|
||
|
||
FOLDER=$1
|
||
KEYWORD=$2
|
||
REMOTE=$3
|
||
shift 3 || true
|
||
|
||
[[ "$KEYWORD" != "from" ]] && usage
|
||
|
||
# Optional flags
|
||
# Purpose: allow custom tunnel port and mount destination.
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
-p|--port)
|
||
[[ $# -lt 2 ]] && usage
|
||
PORT=$2
|
||
shift 2
|
||
;;
|
||
-m|--mount-point)
|
||
[[ $# -lt 2 ]] && usage
|
||
MOUNTPOINT=$2
|
||
shift 2
|
||
;;
|
||
-h|--help)
|
||
usage
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1" >&2
|
||
usage
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# --- normalize and prepare mount point ---
|
||
# Purpose: make the mount point usable (expand ~, absolutize when possible, ensure perms).
|
||
# Expand leading '~' even if quoted or passed via GUI
|
||
# Note: default uses $HOME; still expand '~' if passed via CLI/GUI
|
||
if [[ "${MOUNTPOINT:-}" == "~"* ]]; then
|
||
MOUNTPOINT="${MOUNTPOINT/#\~/$HOME}"
|
||
fi
|
||
# Make absolute if realpath exists (doesn't fail if missing)
|
||
if command -v realpath >/dev/null 2>&1; then
|
||
MOUNTPOINT="$(realpath -m -- "$MOUNTPOINT")"
|
||
fi
|
||
# Create if missing, with restrictive perms
|
||
if [[ ! -d "$MOUNTPOINT" ]]; then
|
||
mkdir -p -- "$MOUNTPOINT"
|
||
chmod 700 -- "$MOUNTPOINT" 2>/dev/null || true
|
||
fi
|
||
# Must be user-writable and empty (warn if non-empty to avoid masking files)
|
||
if [[ ! -w "$MOUNTPOINT" ]]; then
|
||
echo "Mount point '$MOUNTPOINT' is not writable by $(id -un)." >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Warn if non-empty to avoid masking existing files
|
||
if [[ -n "$(ls -A -- "$MOUNTPOINT" 2>/dev/null || true)" ]]; then
|
||
echo "⚠️ Mount point '$MOUNTPOINT' is not empty; its contents will be hidden while mounted." >&2
|
||
fi
|
||
|
||
# --- split remote user/host (supports user:host or user@host) ---
|
||
# Purpose: accept flexible remote formats commonly used in SSH tooling.
|
||
REMOTE_USER=""
|
||
REMOTE_HOST=""
|
||
|
||
if [[ "$REMOTE" == *:* ]]; then
|
||
REMOTE_USER=${REMOTE%%:*}
|
||
REMOTE_HOST=${REMOTE#*:}
|
||
elif [[ "$REMOTE" == *"@"* ]]; then
|
||
REMOTE_USER=${REMOTE%%@*}
|
||
REMOTE_HOST=${REMOTE#*@}
|
||
else
|
||
echo "Invalid remote format. Use remoteuser:remotehost or remoteuser@remotehost" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# --- deps check ---
|
||
# Purpose: fail fast when core tools are missing.
|
||
command -v sshfs >/dev/null 2>&1 || { echo "sshfs not found. Install sshfs first."; exit 1; }
|
||
command -v mountpoint >/dev/null 2>&1 || { echo "mountpoint utility not found."; exit 1; }
|
||
|
||
command -v sftp >/dev/null 2>&1 || { echo "sftp not found (usually provided by openssh)."; exit 1; }
|
||
|
||
# Avoid double-mount
|
||
if mountpoint -q -- "$MOUNTPOINT"; then
|
||
echo "Mount point '$MOUNTPOINT' is already mounted. Unmount it first (e.g., 'fusermount -u \"$MOUNTPOINT\"')." >&2
|
||
exit 1
|
||
fi
|
||
|
||
echo "🔗 Mounting '$FOLDER' from '$REMOTE_USER@$REMOTE_HOST' via reverse-tunnel localhost:$PORT → '$MOUNTPOINT' ..."
|
||
|
||
# --- ensure passwordless auth via tunnel (optional but user-friendly) ---
|
||
# Purpose: detect whether a dedicated identity exists and hint user if passwordless setup is missing.
|
||
SSH_IDENTITY_OPTS=()
|
||
if [[ -f "$HOME/.ssh/id_ed25519_backtunnel" ]]; then
|
||
SSH_IDENTITY_OPTS+=( -o IdentityFile="$HOME/.ssh/id_ed25519_backtunnel" -o IdentitiesOnly=yes )
|
||
fi
|
||
|
||
SFTP_ID_OPTS=()
|
||
if [[ -f "$HOME/.ssh/id_ed25519_backtunnel" ]]; then
|
||
SFTP_ID_OPTS+=( -o IdentityFile="$HOME/.ssh/id_ed25519_backtunnel" -o IdentitiesOnly=yes )
|
||
fi
|
||
|
||
if ! ssh -o BatchMode=yes -o StrictHostKeyChecking="$HKP" \
|
||
-p "$PORT" "${SSH_IDENTITY_OPTS[@]}" "$REMOTE_USER@localhost" true 2>/dev/null; then cat >&2 <<EOF
|
||
⚠️ Passwordless auth not set for $REMOTE_USER@localhost:$PORT.
|
||
You can initialize a tunnel-only, SFTP-only key with:
|
||
backtunnel-auth-setup -p $PORT $REMOTE_USER@localhost
|
||
(It will ask once for the server password to install and restrict the key.)
|
||
EOF
|
||
# continue anyway; sshfs may prompt for password
|
||
fi
|
||
|
||
echo "Checking remote path visibility via SFTP ..."
|
||
|
||
# Purpose: quick sanity check that the target path is visible over SFTP before mounting.
|
||
if ! sftp -q -P "$PORT" -o StrictHostKeyChecking="$HKP" "${SFTP_ID_OPTS[@]}" \
|
||
"$REMOTE_USER@localhost" <<< "ls -1 \"$FOLDER\"" >/dev/null 2>&1; then
|
||
echo "⚠️ Remote path '$FOLDER' not listable via SFTP. It may not exist or permissions deny access." >&2
|
||
echo " Proceeding to mount; sshfs may fail if the path is invalid." >&2
|
||
fi
|
||
|
||
# Build ssh command used by sshfs (adds keepalive/connect-timeout, identity if present).
|
||
SSH_CMD="ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=$HKP"
|
||
# If identity options are present, append them to SSH_CMD
|
||
if [[ ${#SSH_IDENTITY_OPTS[@]} -gt 0 ]]; then
|
||
# Join array safely
|
||
for opt in "${SSH_IDENTITY_OPTS[@]}"; do
|
||
SSH_CMD+=" $opt"
|
||
done
|
||
fi
|
||
|
||
# Perform the mount with reconnect/keepalive options for resilience.
|
||
sshfs \
|
||
-p "$PORT" \
|
||
-o reconnect \
|
||
-o ServerAliveInterval=15 \
|
||
-o ServerAliveCountMax=3 \
|
||
-o ssh_command="$SSH_CMD" \
|
||
-- "$REMOTE_USER@localhost:$FOLDER" "$MOUNTPOINT"
|
||
|
||
echo "✅ Mounted at: $MOUNTPOINT"
|
||
echo "To unmount: backtunnel-umount \"$MOUNTPOINT\""
|