2025-09-14 11:29:21 +02:00
|
|
|
#!/usr/bin/env bash
|
2025-09-21 09:45:43 +02:00
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
# Copyright (c) 2025 LUXIM d.o.o., Slovenia
|
|
|
|
|
# Author: Matjaž Mozetič
|
2025-09-20 17:17:26 +02:00
|
|
|
#
|
2025-09-21 09:45:43 +02:00
|
|
|
# Name: backtunnel-share
|
|
|
|
|
# Summary: Time-bounded reverse-SSH tunnel to expose the local SSH service for remote, temporary SFTP-based access.
|
|
|
|
|
# Description:
|
|
|
|
|
# Sets up a reverse SSH tunnel (ssh -R) from a remote host back to the local sshd, for a fixed duration.
|
|
|
|
|
# Prints an optional “invite” command that the remote side can run to mount a given local folder over SFTP
|
|
|
|
|
# using a companion tool. Optionally and temporarily adds a restricted public key entry to authorized_keys,
|
|
|
|
|
# scoped to localhost and internal-sftp only, and removes it on exit.
|
|
|
|
|
#
|
|
|
|
|
# Usage:
|
|
|
|
|
# backtunnel-share /path/to/folder with remoteuser:remotehost for <duration> [options]
|
|
|
|
|
#
|
|
|
|
|
# Examples:
|
|
|
|
|
# backtunnel-share ~/projects with alice:vps.example.com for 2h
|
|
|
|
|
# backtunnel-share ~/projects with alice@vps.example.com for 1d -p 4422 -l 2222
|
|
|
|
|
# backtunnel-share ~/projects with @work for 2h -i --qr --allow-known alice
|
|
|
|
|
#
|
|
|
|
|
# Dependencies:
|
|
|
|
|
# - bash >= 4.x
|
|
|
|
|
# - ssh, timeout, tail
|
|
|
|
|
# - optional: qrencode (for --qr)
|
|
|
|
|
#
|
|
|
|
|
# Configuration:
|
|
|
|
|
# Profiles file precedence (high → low):
|
|
|
|
|
# - ${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini
|
|
|
|
|
# - /etc/backtunnel/profiles.ini
|
|
|
|
|
# - /usr/share/backtunnel/profiles.ini
|
|
|
|
|
# Sections:
|
|
|
|
|
# - [default] global defaults
|
|
|
|
|
# - [name] referenced via @name → expands to user@host and overrides select defaults
|
|
|
|
|
#
|
|
|
|
|
# Exit codes:
|
|
|
|
|
# 0 success (including duration reached)
|
|
|
|
|
# 1 invalid usage/arguments or validation failure
|
|
|
|
|
# 124 timeout reached (handled and normalized to 0 when expected)
|
|
|
|
|
# 2+ other runtime errors (from commands like ssh/timeout/tail)
|
|
|
|
|
#
|
|
|
|
|
# Security:
|
|
|
|
|
# - Reverse tunnel binds on remote loopback only (127.0.0.1:PORT).
|
|
|
|
|
# - Temporary authorized_keys entries (if enabled) are restricted with:
|
|
|
|
|
# from="127.0.0.1",command="internal-sftp",restrict
|
|
|
|
|
# and are removed on exit via trap.
|
|
|
|
|
# - ~/.ssh perms are enforced (700 dir, 600 authorized_keys).
|
|
|
|
|
#
|
|
|
|
|
# Portability/Assumptions:
|
|
|
|
|
# - Uses bash arrays, [[ ]] tests, and bash-specific parameter expansions.
|
|
|
|
|
# - Uses awk for INI parsing (best-effort).
|
|
|
|
|
#
|
|
|
|
|
# Notes:
|
|
|
|
|
# - set -euo pipefail: stop on error/undefined vars; treat pipeline errors as failures.
|
|
|
|
|
# - Traps ensure SSH child is terminated and temporary keys are cleaned up.
|
|
|
|
|
|
2025-09-14 11:29:21 +02:00
|
|
|
# backtunnel-share: Share a folder using reverse SSH for a limited duration
|
2025-09-20 17:17:26 +02:00
|
|
|
# Syntax:
|
|
|
|
|
# backtunnel-share /path/to/folder with remoteuser:remotehost for 2h [options]
|
|
|
|
|
#
|
|
|
|
|
# Options:
|
|
|
|
|
# -p|--tunnel-port <PORT> Remote port to expose with -R (default: 2222)
|
|
|
|
|
# -l|--local-ssh-port <PORT> Local sshd port to forward to (default: 22)
|
|
|
|
|
# -i|--invite Print an invite command (and/or QR) for chat
|
|
|
|
|
# --invite-mount <PATH> Suggested mount point in invite (default: $HOME/remote-rssh)
|
|
|
|
|
# --invite-file <FILE> Also write invite text (with unmount hint) to FILE
|
|
|
|
|
# --qr Also render the invite command as a QR code (qrencode)
|
|
|
|
|
# --allow-key <FILE> Temporarily authorize accessor's public key for this session
|
|
|
|
|
# --allow-known <NAME> Temporarily authorize ~/.config/backtunnel/authorized/NAME.pub
|
|
|
|
|
# -h|--help Show help
|
|
|
|
|
#
|
|
|
|
|
# Profiles: ~/.config/backtunnel/profiles.ini (overrides /etc/backtunnel/profiles.ini then /usr/share/backtunnel/profiles.ini)
|
|
|
|
|
# [default] tunnel_port=2222 local_ssh_port=22 invite_mount=$HOME/remote-rssh invite=true qr=false
|
|
|
|
|
# [alice] user=alice host=vps.example.com tunnel_port=4422
|
2025-09-14 11:29:21 +02:00
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Config discovery
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: choose the highest-precedence profiles.ini available.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 19:44:37 +02:00
|
|
|
CONFIG_USER="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini"
|
|
|
|
|
CONFIG_SYS="/etc/backtunnel/profiles.ini"
|
|
|
|
|
CONFIG_PKG="/usr/share/backtunnel/profiles.ini"
|
|
|
|
|
if [[ -f "$CONFIG_USER" ]]; then
|
|
|
|
|
CONFIG_FILE="$CONFIG_USER"
|
|
|
|
|
elif [[ -f "$CONFIG_SYS" ]]; then
|
|
|
|
|
CONFIG_FILE="$CONFIG_SYS"
|
|
|
|
|
else
|
|
|
|
|
CONFIG_FILE="$CONFIG_PKG"
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# INI helpers
|
|
|
|
|
# ----------------------------
|
|
|
|
|
# shellcheck disable=SC2317
|
2025-09-21 09:45:43 +02:00
|
|
|
# ini_get: read a value from CONFIG_FILE INI [SECTION] key=value
|
|
|
|
|
# Arguments:
|
|
|
|
|
# $1: section name
|
|
|
|
|
# $2: key
|
|
|
|
|
# Env:
|
|
|
|
|
# CONFIG_FILE (read)
|
|
|
|
|
# Returns:
|
|
|
|
|
# prints the value on success; empty string if not found
|
|
|
|
|
# Notes:
|
|
|
|
|
# - Best-effort parsing; trims spaces around '=' and the value.
|
2025-09-14 19:44:37 +02:00
|
|
|
ini_get() { # ini_get SECTION KEY -> value
|
|
|
|
|
local sec="$1" key="$2"
|
|
|
|
|
awk -v s="[""$sec""]" -v k="$key" '
|
|
|
|
|
$0==s {ok=1; next}
|
|
|
|
|
/^\[/ {ok=0}
|
|
|
|
|
ok && $0 ~ /^[[:alnum:]_.-]+[[:space:]]*=/ {
|
|
|
|
|
line=$0
|
|
|
|
|
sub(/[[:space:]]*=[[:space:]]*/, "=", line)
|
|
|
|
|
split(line,a,"=")
|
|
|
|
|
if (a[1]==k) {
|
|
|
|
|
val=substr(line, index(line,"=")+1)
|
|
|
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/,"",val)
|
|
|
|
|
print val
|
|
|
|
|
exit
|
|
|
|
|
}
|
|
|
|
|
}' "${CONFIG_FILE}" 2>/dev/null || true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# shellcheck disable=SC2317
|
2025-09-21 09:45:43 +02:00
|
|
|
# profile_expand_remote: expand @name -> user@host based on profiles.ini, else pass through.
|
|
|
|
|
# Arguments:
|
|
|
|
|
# $1: input remote spec (e.g., @work or user@host or user:host)
|
|
|
|
|
# Returns:
|
|
|
|
|
# prints expanded remote (user@host) if @name is resolvable; otherwise returns input unchanged
|
2025-09-14 19:44:37 +02:00
|
|
|
profile_expand_remote() { # "@name" -> user@host, otherwise pass through
|
|
|
|
|
local in="$1"
|
|
|
|
|
if [[ "$in" == @* ]]; then
|
|
|
|
|
local name="${in#@}" user host
|
|
|
|
|
user="$(ini_get "$name" user)"
|
|
|
|
|
host="$(ini_get "$name" host)"
|
|
|
|
|
if [[ -n "$user" && -n "$host" ]]; then
|
|
|
|
|
printf '%s@%s\n' "$user" "$host"
|
|
|
|
|
return 0
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
printf '%s\n' "$in"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# shellcheck disable=SC2317
|
2025-09-21 09:45:43 +02:00
|
|
|
# profile_apply_defaults: set global defaults from [default], then override from named profile.
|
|
|
|
|
# Arguments:
|
|
|
|
|
# $1: profile name (without '@'), may be empty
|
|
|
|
|
# Side effects:
|
|
|
|
|
# - Modifies global variables: TUNNEL_PORT, LOCAL_SSH_PORT, INVITE_MOUNT, INVITE, QR, DURATION
|
|
|
|
|
# Notes:
|
|
|
|
|
# - Only applies default values if globals still hold built-in defaults.
|
2025-09-14 19:44:37 +02:00
|
|
|
profile_apply_defaults() { # set globals if unset; named overrides default
|
|
|
|
|
local name="$1" v
|
|
|
|
|
# defaults
|
|
|
|
|
v="$(ini_get default tunnel_port)"; [[ -n "$v" && "${TUNNEL_PORT}" == "2222" ]] && TUNNEL_PORT="$v"
|
|
|
|
|
v="$(ini_get default local_ssh_port)"; [[ -n "$v" && "${LOCAL_SSH_PORT}" == "22" ]] && LOCAL_SSH_PORT="$v"
|
2025-09-20 10:49:45 +02:00
|
|
|
v="$(ini_get default invite_mount)"; [[ -n "$v" && "${INVITE_MOUNT}" == "$HOME/remote-rssh" ]] && INVITE_MOUNT="$v"
|
2025-09-14 19:44:37 +02:00
|
|
|
v="$(ini_get default invite)"; [[ "${v,,}" == "true" ]] && INVITE=true
|
|
|
|
|
v="$(ini_get default qr)"; [[ "${v,,}" == "true" ]] && QR=true
|
|
|
|
|
if [[ -z "$DURATION" ]]; then
|
|
|
|
|
v="$(ini_get default duration)"; [[ -n "$v" ]] && DURATION="$v"
|
|
|
|
|
fi
|
|
|
|
|
# named section
|
|
|
|
|
if [[ -n "$name" ]]; then
|
|
|
|
|
v="$(ini_get "$name" tunnel_port)"; [[ -n "$v" ]] && TUNNEL_PORT="$v"
|
|
|
|
|
v="$(ini_get "$name" local_ssh_port)"; [[ -n "$v" ]] && LOCAL_SSH_PORT="$v"
|
|
|
|
|
v="$(ini_get "$name" invite_mount)"; [[ -n "$v" ]] && INVITE_MOUNT="$v"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Defaults
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: initialize built-in defaults before applying profiles and flags.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
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
|
2025-09-14 19:44:37 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
INVITE=false # print a ready-to-copy access command
|
2025-09-20 10:49:45 +02:00
|
|
|
INVITE_MOUNT="$HOME/remote-rssh"
|
2025-09-14 12:54:06 +02:00
|
|
|
INVITE_FILE=""
|
2025-09-20 17:17:26 +02:00
|
|
|
QR=false # render invite as terminal QR (requires qrencode)
|
|
|
|
|
|
|
|
|
|
# Accessor authorization (session-scoped)
|
|
|
|
|
ALLOW_KEY_FILE=""
|
|
|
|
|
ALLOW_KNOWN_NAME=""
|
|
|
|
|
AUTHORIZED_STORE="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/authorized"
|
|
|
|
|
ADDED_MARKER_ID="" # unique ID for cleanup removal
|
2025-09-14 12:54:06 +02:00
|
|
|
|
2025-09-14 11:29:21 +02:00
|
|
|
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
|
2025-09-20 17:17:26 +02:00
|
|
|
remoteuser:remotehost Or remoteuser@remotehost (or @profile)
|
2025-09-14 11:29:21 +02:00
|
|
|
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})
|
2025-09-14 12:54:06 +02:00
|
|
|
-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')
|
2025-09-20 17:17:26 +02:00
|
|
|
|
|
|
|
|
--allow-key FILE Authorize this public key for this session (restricted SFTP-only, tunnel-only)
|
|
|
|
|
--allow-known NAME Authorize ~/.config/backtunnel/authorized/NAME.pub
|
|
|
|
|
|
2025-09-14 11:29:21 +02:00
|
|
|
-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
|
2025-09-20 17:17:26 +02:00
|
|
|
$(basename "$0") ~/projects with @work for 2h -i --qr --allow-known alice
|
2025-09-14 11:29:21 +02:00
|
|
|
EOF
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Positional parsing
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: enforce command grammar and capture the five required positionals.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 11:29:21 +02:00
|
|
|
[[ $# -lt 5 ]] && usage
|
|
|
|
|
|
|
|
|
|
FOLDER=$1
|
|
|
|
|
KW1=$2
|
|
|
|
|
REMOTE=$3
|
|
|
|
|
KW2=$4
|
|
|
|
|
DURATION=$5
|
|
|
|
|
shift 5 || true
|
|
|
|
|
|
|
|
|
|
[[ "$KW1" != "with" ]] && usage
|
|
|
|
|
[[ "$KW2" != "for" ]] && usage
|
|
|
|
|
|
2025-09-14 19:44:37 +02:00
|
|
|
# Apply [default] + named profile (if REMOTE is @name) before flag overrides
|
|
|
|
|
profile_name=""
|
|
|
|
|
if [[ "$REMOTE" == @* ]]; then
|
|
|
|
|
profile_name="${REMOTE#@}"
|
|
|
|
|
fi
|
|
|
|
|
profile_apply_defaults "$profile_name"
|
|
|
|
|
# Expand @name -> user@host for actual ssh use
|
|
|
|
|
REMOTE="$(profile_expand_remote "$REMOTE")"
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Optional flags
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: parse options and override derived defaults.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 11:29:21 +02:00
|
|
|
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
|
|
|
|
|
;;
|
2025-09-14 12:54:06 +02:00
|
|
|
-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
|
|
|
|
|
;;
|
2025-09-20 17:17:26 +02:00
|
|
|
--allow-key)
|
|
|
|
|
[[ $# -lt 2 ]] && usage
|
|
|
|
|
ALLOW_KEY_FILE=$2
|
|
|
|
|
shift 2
|
|
|
|
|
;;
|
|
|
|
|
--allow-known)
|
|
|
|
|
[[ $# -lt 2 ]] && usage
|
|
|
|
|
ALLOW_KNOWN_NAME=$2
|
|
|
|
|
shift 2
|
|
|
|
|
;;
|
2025-09-14 11:29:21 +02:00
|
|
|
-h|--help)
|
|
|
|
|
usage
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
echo "Unknown option: $1" >&2
|
|
|
|
|
usage
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
done
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Duration validation
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: accept forms like 30m, 2h, 1d (s/m/h/d).
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 11:29:21 +02:00
|
|
|
if [[ ! "$DURATION" =~ ^[0-9]+[smhd]$ ]]; then
|
|
|
|
|
echo "Invalid duration '$DURATION' (use forms like 30m, 2h, 1d)." >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Split remote user/host
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: support user:host or user@host; reject anything else (except @profile pre-expanded).
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 11:29:21 +02:00
|
|
|
REMOTE_USER="" REMOTE_HOST=""
|
|
|
|
|
if [[ "$REMOTE" == *:* ]]; then
|
|
|
|
|
REMOTE_USER=${REMOTE%%:*}
|
|
|
|
|
REMOTE_HOST=${REMOTE#*:}
|
|
|
|
|
elif [[ "$REMOTE" == *"@"* ]]; then
|
|
|
|
|
REMOTE_USER=${REMOTE%%@*}
|
|
|
|
|
REMOTE_HOST=${REMOTE#*@}
|
|
|
|
|
else
|
2025-09-20 17:17:26 +02:00
|
|
|
echo "Invalid remote format. Use remoteuser:remotehost or remoteuser@remotehost or @profile" >&2
|
2025-09-14 11:29:21 +02:00
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Deps check
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: fail fast if mandatory tools are missing.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 11:29:21 +02:00
|
|
|
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; }
|
2025-09-20 17:17:26 +02:00
|
|
|
command -v tail >/dev/null 2>&1 || { echo "tail not found."; exit 1; }
|
|
|
|
|
|
|
|
|
|
# ----------------------------
|
|
|
|
|
# Accessor temporary authorization
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: optionally add and later remove a restricted authorized_keys entry.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-21 09:45:43 +02:00
|
|
|
# restrict_key_line: produce a hardened authorized_keys line for an OpenSSH public key.
|
|
|
|
|
# Arguments:
|
|
|
|
|
# $1: public key text (one line, ssh-*)
|
|
|
|
|
# Returns:
|
|
|
|
|
# prints a restricted options prefix + key; nonzero if key is invalid
|
2025-09-20 17:17:26 +02:00
|
|
|
restrict_key_line() { # usage: restrict_key_line <pubkey_text>
|
|
|
|
|
local pk="$1"
|
|
|
|
|
# Normalize
|
|
|
|
|
pk="${pk//$'\r'/}"
|
|
|
|
|
pk="${pk//$'\n'/}"
|
|
|
|
|
if [[ "$pk" != ssh-* ]]; then
|
|
|
|
|
echo "Provided key does not look like an OpenSSH public key." >&2
|
|
|
|
|
return 1
|
|
|
|
|
fi
|
|
|
|
|
printf 'from="127.0.0.1",command="internal-sftp",restrict %s' "$pk"
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-21 09:45:43 +02:00
|
|
|
# add_temp_authorized_key: append a uniquely marked restricted key block to ~/.ssh/authorized_keys.
|
|
|
|
|
# Arguments:
|
|
|
|
|
# $1: public key text
|
|
|
|
|
# Side effects:
|
|
|
|
|
# - Ensures ~/.ssh perms (700) and authorized_keys perms (600)
|
|
|
|
|
# - Writes marker lines and sets ADDED_MARKER_ID for later removal
|
2025-09-20 17:17:26 +02:00
|
|
|
add_temp_authorized_key() {
|
|
|
|
|
local pubkey_text="$1"
|
|
|
|
|
local ak="$HOME/.ssh/authorized_keys"
|
|
|
|
|
# SC2155: Declare and assign separately to avoid masking return values.
|
|
|
|
|
# shellcheck disable=SC2155
|
|
|
|
|
local marker
|
|
|
|
|
marker="BACKTUNNEL-TEMP-$(date +%s)-$$-$RANDOM"
|
|
|
|
|
local start="# ${marker} START"
|
|
|
|
|
local end="# ${marker} END"
|
|
|
|
|
|
|
|
|
|
mkdir -p "$HOME/.ssh"
|
|
|
|
|
touch "$ak"
|
|
|
|
|
chmod 700 "$HOME/.ssh"
|
|
|
|
|
chmod 600 "$ak"
|
|
|
|
|
|
|
|
|
|
local restricted
|
|
|
|
|
restricted="$(restrict_key_line "$pubkey_text")" || return 1
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
echo "$start"
|
|
|
|
|
echo "$restricted"
|
|
|
|
|
echo "$end"
|
|
|
|
|
} >> "$ak"
|
|
|
|
|
|
|
|
|
|
ADDED_MARKER_ID="$marker"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# shellcheck disable=SC2317
|
2025-09-21 09:45:43 +02:00
|
|
|
# remove_temp_authorized_key: delete the previously added marked block from authorized_keys.
|
|
|
|
|
# Env:
|
|
|
|
|
# ADDED_MARKER_ID (read)
|
|
|
|
|
# Notes:
|
|
|
|
|
# No-op if no marker or authorized_keys missing. Best-effort; ignores errors.
|
2025-09-20 17:17:26 +02:00
|
|
|
remove_temp_authorized_key() {
|
|
|
|
|
local ak="$HOME/.ssh/authorized_keys"
|
|
|
|
|
[[ -z "${ADDED_MARKER_ID:-}" ]] && return 0
|
|
|
|
|
[[ -f "$ak" ]] || return 0
|
|
|
|
|
awk -v m="$ADDED_MARKER_ID" '
|
|
|
|
|
$0 ~ "# " m " START" {skip=1; next}
|
|
|
|
|
$0 ~ "# " m " END" {skip=0; next}
|
|
|
|
|
!skip {print}
|
|
|
|
|
' "$ak" > "${ak}.btnew" && mv "${ak}.btnew" "$ak"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ACCESSOR_PUBKEY_TEXT=""
|
|
|
|
|
|
|
|
|
|
if [[ -n "$ALLOW_KNOWN_NAME" ]]; then
|
|
|
|
|
f="${AUTHORIZED_STORE}/${ALLOW_KNOWN_NAME}.pub"
|
|
|
|
|
if [[ -f "$f" ]]; then
|
|
|
|
|
ACCESSOR_PUBKEY_TEXT="$(cat "$f")"
|
|
|
|
|
else
|
|
|
|
|
echo "Known accessor key not found: $f" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
fi
|
2025-09-14 11:29:21 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
if [[ -n "$ALLOW_KEY_FILE" ]]; then
|
|
|
|
|
if [[ -f "$ALLOW_KEY_FILE" ]]; then
|
|
|
|
|
ACCESSOR_PUBKEY_TEXT="$(cat "$ALLOW_KEY_FILE")"
|
|
|
|
|
else
|
|
|
|
|
echo "Given key file not found: $ALLOW_KEY_FILE" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ -n "$ACCESSOR_PUBKEY_TEXT" ]]; then
|
|
|
|
|
add_temp_authorized_key "$ACCESSOR_PUBKEY_TEXT"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# ----------------------------
|
|
|
|
|
# Banner
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: inform the user what will happen and where to connect from the remote.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 11:29:21 +02:00
|
|
|
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}"
|
2025-09-20 17:17:26 +02:00
|
|
|
if [[ -n "$ACCESSOR_PUBKEY_TEXT" ]]; then
|
|
|
|
|
echo " accessor key : temporarily authorized (restricted)"
|
|
|
|
|
fi
|
2025-09-14 11:29:21 +02:00
|
|
|
echo
|
2025-09-14 12:54:06 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Invite (optional)
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: print a ready-to-copy command (and QR) for the remote side.
|
|
|
|
|
# Notes:
|
|
|
|
|
# If a key was not pre-authorized, prepend an auth-setup step executed over the tunnel.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 12:54:06 +02:00
|
|
|
if $INVITE; then
|
|
|
|
|
INVITE_CMD="backtunnel-access '${FOLDER}' from ${REMOTE_USER}@${REMOTE_HOST} -p ${TUNNEL_PORT} -m '${INVITE_MOUNT}'"
|
2025-09-20 10:49:45 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
if [[ -z "$ACCESSOR_PUBKEY_TEXT" ]]; then
|
|
|
|
|
AUTH_CMD="backtunnel-auth-setup -p ${TUNNEL_PORT} ${REMOTE_USER}@localhost"
|
|
|
|
|
INVITE_TEXT=$(
|
|
|
|
|
cat <<EOT
|
2025-09-20 10:49:45 +02:00
|
|
|
|
|
|
|
|
# 1) (one-time) install a tunnel-only, SFTP-only key via the reverse tunnel:
|
|
|
|
|
${AUTH_CMD}
|
|
|
|
|
|
|
|
|
|
# 2) mount the share:
|
2025-09-14 12:54:06 +02:00
|
|
|
${INVITE_CMD}
|
2025-09-20 10:49:45 +02:00
|
|
|
|
2025-09-14 12:54:06 +02:00
|
|
|
# Unmount when done:
|
2025-09-14 20:41:51 +02:00
|
|
|
fusermount -u '${INVITE_MOUNT}' || fusermount3 -u '${INVITE_MOUNT}'
|
2025-09-20 17:17:26 +02:00
|
|
|
EOT
|
|
|
|
|
)
|
|
|
|
|
else
|
|
|
|
|
AUTH_CMD=""
|
|
|
|
|
INVITE_TEXT=$(
|
|
|
|
|
cat <<EOT
|
|
|
|
|
|
|
|
|
|
# Mount the share:
|
|
|
|
|
${INVITE_CMD}
|
2025-09-20 10:49:45 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# Unmount when done:
|
|
|
|
|
fusermount -u '${INVITE_MOUNT}' || fusermount3 -u '${INVITE_MOUNT}'
|
2025-09-14 12:54:06 +02:00
|
|
|
EOT
|
2025-09-20 17:17:26 +02:00
|
|
|
)
|
|
|
|
|
fi
|
2025-09-20 10:49:45 +02:00
|
|
|
|
2025-09-14 12:54:06 +02:00
|
|
|
echo "🔗 Invite (copy to chat):"
|
|
|
|
|
echo "------------------------------------------------------------"
|
2025-09-20 17:17:26 +02:00
|
|
|
[[ -n "$AUTH_CMD" ]] && echo "${AUTH_CMD}"
|
2025-09-14 12:54:06 +02:00
|
|
|
echo "${INVITE_CMD}"
|
|
|
|
|
echo "------------------------------------------------------------"
|
2025-09-20 10:49:45 +02:00
|
|
|
|
2025-09-14 12:54:06 +02:00
|
|
|
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
|
|
|
|
|
|
2025-09-14 11:29:21 +02:00
|
|
|
echo "Tip: On the remote side, mount with:"
|
|
|
|
|
echo " backtunnel-access '${FOLDER}' from ${REMOTE_USER}@${REMOTE_HOST} -p ${TUNNEL_PORT}"
|
2025-09-14 18:07:58 +02:00
|
|
|
echo "Listener will appear on: ${REMOTE_HOST}:${TUNNEL_PORT} (run access there)."
|
|
|
|
|
echo "To stop sharing early: press Ctrl+C in this window."
|
2025-09-14 14:11:36 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Pre-flight: warn if remote loopback port already in use (best-effort)
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: give an actionable warning before attempting the -R bind.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 14:11:36 +02:00
|
|
|
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
|
2025-09-20 17:17:26 +02:00
|
|
|
# You may 'exit 1' here if you prefer a hard failure
|
2025-09-14 14:11:36 +02:00
|
|
|
fi
|
2025-09-14 11:29:21 +02:00
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
|
|
|
|
# Cleanup & run SSH
|
2025-09-21 09:45:43 +02:00
|
|
|
# Purpose: start the tunnel, wait bounded by duration, and guarantee cleanup via trap.
|
2025-09-20 17:17:26 +02:00
|
|
|
# ----------------------------
|
2025-09-14 18:07:58 +02:00
|
|
|
SSH_PID=""
|
|
|
|
|
|
2025-09-20 17:17:26 +02:00
|
|
|
# shellcheck disable=SC2317
|
2025-09-21 09:45:43 +02:00
|
|
|
# cleanup: stop the background ssh process and remove any temporary authorized key.
|
|
|
|
|
# Notes:
|
|
|
|
|
# - Invoked on INT/TERM/EXIT to ensure resources are released.
|
2025-09-14 18:07:58 +02:00
|
|
|
cleanup() {
|
2025-09-20 17:17:26 +02:00
|
|
|
# stop ssh child if running
|
2025-09-14 18:07:58 +02:00
|
|
|
if [[ -n "${SSH_PID:-}" ]] && kill -0 "$SSH_PID" 2>/dev/null; then
|
|
|
|
|
echo "⏹️ Stopping share..."
|
|
|
|
|
kill -TERM "$SSH_PID" 2>/dev/null || true
|
|
|
|
|
wait "$SSH_PID" 2>/dev/null || true
|
|
|
|
|
fi
|
2025-09-20 17:17:26 +02:00
|
|
|
# remove temporary authorized key if we added one
|
|
|
|
|
remove_temp_authorized_key
|
2025-09-14 18:07:58 +02:00
|
|
|
}
|
|
|
|
|
trap cleanup INT TERM EXIT
|
|
|
|
|
|
|
|
|
|
# Start reverse-tunnel ssh in background with keepalives + fast-fail on -R bind
|
|
|
|
|
ssh -N \
|
2025-09-14 14:11:36 +02:00
|
|
|
-o ExitOnForwardFailure=yes \
|
|
|
|
|
-o ServerAliveInterval=15 \
|
|
|
|
|
-o ServerAliveCountMax=3 \
|
2025-09-14 11:29:21 +02:00
|
|
|
-R "${TUNNEL_PORT}:localhost:${LOCAL_SSH_PORT}" \
|
2025-09-14 18:07:58 +02:00
|
|
|
-- "${REMOTE_USER}@${REMOTE_HOST}" &
|
|
|
|
|
SSH_PID=$!
|
2025-09-14 11:29:21 +02:00
|
|
|
|
2025-09-14 18:07:58 +02:00
|
|
|
# Wait for ssh to exit, but bounded by duration:
|
2025-09-21 09:45:43 +02:00
|
|
|
# Rationale: use 'timeout … tail --pid=PID -f /dev/null' to avoid subshell/wait-loop complexity.
|
2025-09-14 18:07:58 +02:00
|
|
|
if timeout "$DURATION" tail --pid="$SSH_PID" -f /dev/null; then
|
|
|
|
|
# ssh exited on its own before timeout
|
2025-09-14 11:29:21 +02:00
|
|
|
exit 0
|
2025-09-14 18:07:58 +02:00
|
|
|
else
|
|
|
|
|
rc=$?
|
|
|
|
|
if [[ $rc -eq 124 ]]; then
|
|
|
|
|
echo "⏹️ Sharing ended: reached duration (${DURATION})."
|
|
|
|
|
# ensure the child is gone
|
|
|
|
|
if kill -0 "$SSH_PID" 2>/dev/null; then
|
|
|
|
|
kill -TERM "$SSH_PID" 2>/dev/null || true
|
|
|
|
|
wait "$SSH_PID" 2>/dev/null || true
|
|
|
|
|
fi
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
# other error from timeout/tail
|
|
|
|
|
exit "$rc"
|
2025-09-14 11:29:21 +02:00
|
|
|
fi
|