193 lines
5.8 KiB
Bash
193 lines
5.8 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# Copyright (c) 2025. LUXIM d.o.o., Slovenia - Matjaž Mozetič.
|
|
# Licensed under the GNU GPL v3.0
|
|
|
|
# backtunnel-share: Share a folder using reverse SSH for a limited duration
|
|
# Syntax: backtunnel-share /path/to/folder with remoteuser:remotehost for 2h
|
|
# Options: -p|--tunnel-port <PORT> -l|--local-ssh-port <PORT> -i|--invite [--invite-mount <PATH>] [--invite-file <FILE>] [--qr] -h|--help
|
|
|
|
set -euo pipefail
|
|
|
|
TUNNEL_PORT=2222 # remote-side port exposed via -R
|
|
LOCAL_SSH_PORT=22 # local sshd port to forward to
|
|
DURATION="" # required: e.g. 30m, 2h, 1d
|
|
|
|
INVITE=false # print a ready-to-copy access command
|
|
INVITE_MOUNT="/mnt/remote-rssh"
|
|
INVITE_FILE=""
|
|
QR=false # render invite as terminal QR (requires qrencode)
|
|
|
|
usage() {
|
|
cat >&2 <<EOF
|
|
Usage:
|
|
$(basename "$0") /path/to/folder with remoteuser:remotehost for <duration> [options]
|
|
|
|
Positional (required, in order):
|
|
/path/to/folder Informational only (the actual folder is mounted by backtunnel-access)
|
|
with Literal keyword
|
|
remoteuser:remotehost Or remoteuser@remotehost
|
|
for Literal keyword
|
|
<duration> e.g. 30m, 2h, 1d (passed to 'timeout')
|
|
|
|
Options:
|
|
-p, --tunnel-port N Remote tunnel port (default: ${TUNNEL_PORT})
|
|
-l, --local-ssh-port N Local sshd port to expose (default: ${LOCAL_SSH_PORT})
|
|
-i, --invite Print a ready-to-copy access command for the remote side
|
|
--invite-mount PATH Mount point suggested in invite (default: ${INVITE_MOUNT})
|
|
--invite-file FILE Also write the invite text (with unmount hint) to FILE
|
|
--qr Also print a QR code (requires 'qrencode')
|
|
-h, --help Show this help
|
|
|
|
Examples:
|
|
$(basename "$0") ~/projects with alice:vps.example.com for 2h
|
|
$(basename "$0") ~/projects with alice@vps.example.com for 1d -p 4422 -l 2222
|
|
$(basename "$0") ~/projects with alice@vps.example.com for 2h -i --qr
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
# --- basic positional parsing ---
|
|
[[ $# -lt 5 ]] && usage
|
|
|
|
FOLDER=$1
|
|
KW1=$2
|
|
REMOTE=$3
|
|
KW2=$4
|
|
DURATION=$5
|
|
shift 5 || true
|
|
|
|
[[ "$KW1" != "with" ]] && usage
|
|
[[ "$KW2" != "for" ]] && usage
|
|
|
|
# --- optional flags ---
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-p|--tunnel-port)
|
|
[[ $# -lt 2 ]] && usage
|
|
TUNNEL_PORT=$2
|
|
shift 2
|
|
;;
|
|
-l|--local-ssh-port)
|
|
[[ $# -lt 2 ]] && usage
|
|
LOCAL_SSH_PORT=$2
|
|
shift 2
|
|
;;
|
|
-i|--invite)
|
|
INVITE=true
|
|
shift
|
|
;;
|
|
--invite-mount)
|
|
[[ $# -lt 2 ]] && usage
|
|
INVITE_MOUNT=$2
|
|
shift 2
|
|
;;
|
|
--invite-file)
|
|
[[ $# -lt 2 ]] && usage
|
|
INVITE_FILE=$2
|
|
shift 2
|
|
;;
|
|
--qr)
|
|
QR=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# --- validate duration (timeout supports s,m,h,d) ---
|
|
if [[ ! "$DURATION" =~ ^[0-9]+[smhd]$ ]]; then
|
|
echo "Invalid duration '$DURATION' (use forms like 30m, 2h, 1d)." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- split remote user/host ---
|
|
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 ---
|
|
command -v ssh >/dev/null 2>&1 || { echo "ssh not found."; exit 1; }
|
|
command -v timeout >/dev/null 2>&1 || { echo "timeout not found."; exit 1; }
|
|
|
|
echo "⏳ Sharing '${FOLDER}' via reverse SSH:"
|
|
echo " local sshd port : ${LOCAL_SSH_PORT}"
|
|
echo " remote bind port : ${TUNNEL_PORT} (on ${REMOTE_HOST})"
|
|
echo " remote user : ${REMOTE_USER}"
|
|
echo " duration : ${DURATION}"
|
|
echo
|
|
|
|
# --- print invite (optional) ---
|
|
if $INVITE; then
|
|
INVITE_CMD="backtunnel-access '${FOLDER}' from ${REMOTE_USER}@${REMOTE_HOST} -p ${TUNNEL_PORT} -m '${INVITE_MOUNT}'"
|
|
INVITE_TEXT=$(
|
|
cat <<EOT
|
|
# Paste this on the REMOTE host (or SSH there first, then paste):
|
|
${INVITE_CMD}
|
|
# Unmount when done:
|
|
fusermount -u '${INVITE_MOUNT}'
|
|
EOT
|
|
)
|
|
echo "🔗 Invite (copy to chat):"
|
|
echo "------------------------------------------------------------"
|
|
echo "${INVITE_CMD}"
|
|
echo "------------------------------------------------------------"
|
|
if [[ -n "${INVITE_FILE}" ]]; then
|
|
printf "%s\n" "${INVITE_TEXT}" > "${INVITE_FILE}"
|
|
echo "Saved invite to: ${INVITE_FILE}"
|
|
fi
|
|
if $QR; then
|
|
if command -v qrencode >/dev/null 2>&1; then
|
|
echo
|
|
echo "📱 QR (scan to copy the command):"
|
|
printf "%s" "${INVITE_CMD}" | qrencode -t ansiutf8
|
|
else
|
|
echo "⚠️ 'qrencode' not installed; skipping QR."
|
|
fi
|
|
fi
|
|
echo
|
|
fi
|
|
|
|
echo "Tip: On the remote side, mount with:"
|
|
echo " backtunnel-access '${FOLDER}' from ${REMOTE_USER}@${REMOTE_HOST} -p ${TUNNEL_PORT}"
|
|
echo "To stop sharing early: press Ctrl+C here."
|
|
|
|
# --- optional pre-flight: warn if remote loopback port is already in use (best-effort) ---
|
|
if ssh -o BatchMode=yes -o ConnectTimeout=5 "${REMOTE_USER}@${REMOTE_HOST}" \
|
|
"command -v nc >/dev/null 2>&1 && nc -z 127.0.0.1 ${TUNNEL_PORT}"; then
|
|
echo "⚠️ Port ${TUNNEL_PORT} on remote 127.0.0.1 appears in use; choose another with -p." >&2
|
|
# Uncomment the next line to make it a hard failure:
|
|
# exit 1
|
|
fi
|
|
|
|
# Bind reverse to 127.0.0.1 on the REMOTE host (private-by-default).
|
|
# Keepalive so the tunnel drops on dead links.
|
|
# Exit fast if the reverse bind fails (e.g., port in use).
|
|
timeout "$DURATION" ssh -N \
|
|
-o ExitOnForwardFailure=yes \
|
|
-o ServerAliveInterval=15 \
|
|
-o ServerAliveCountMax=3 \
|
|
-R "${TUNNEL_PORT}:localhost:${LOCAL_SSH_PORT}" \
|
|
-- "${REMOTE_USER}@${REMOTE_HOST}"
|
|
|
|
rc=$?
|
|
if [[ $rc -eq 124 ]]; then
|
|
echo "⏹️ Sharing ended: reached duration (${DURATION})."
|
|
exit 0
|
|
fi
|
|
exit "$rc"
|