#!/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" usage() { echo "Usage: $0 /path/to/folder from remoteuser:remotehost [-p PORT] [-m MOUNTPOINT]" >&2 exit 1 } # --- parse positional args --- # Purpose: enforce grammar " from " 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=accept-new \ -p "$PORT" "${SSH_IDENTITY_OPTS[@]}" "$REMOTE_USER@localhost" true 2>/dev/null; then cat >&2 </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=accept-new" # 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: fusermount -u \"$MOUNTPOINT\" || fusermount3 -u \"$MOUNTPOINT\""