Add comprehensive inline metadata documentation to all BackTunnel scripts
This commit is contained in:
@@ -1,7 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) 2025. LUXIM d.o.o., Slovenia - Matjaž Mozetič.
|
||||
# Licensed under the GNU GPL v3.0
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# Copyright (c) 2025 LUXIM d.o.o., Slovenia
|
||||
# Author: Matjaž Mozetič
|
||||
#
|
||||
# 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.
|
||||
|
||||
# backtunnel-share: Share a folder using reverse SSH for a limited duration
|
||||
# Syntax:
|
||||
# backtunnel-share /path/to/folder with remoteuser:remotehost for 2h [options]
|
||||
@@ -25,6 +77,7 @@ set -euo pipefail
|
||||
|
||||
# ----------------------------
|
||||
# Config discovery
|
||||
# Purpose: choose the highest-precedence profiles.ini available.
|
||||
# ----------------------------
|
||||
CONFIG_USER="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini"
|
||||
CONFIG_SYS="/etc/backtunnel/profiles.ini"
|
||||
@@ -41,6 +94,16 @@ fi
|
||||
# INI helpers
|
||||
# ----------------------------
|
||||
# shellcheck disable=SC2317
|
||||
# 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.
|
||||
ini_get() { # ini_get SECTION KEY -> value
|
||||
local sec="$1" key="$2"
|
||||
awk -v s="[""$sec""]" -v k="$key" '
|
||||
@@ -60,6 +123,11 @@ ini_get() { # ini_get SECTION KEY -> value
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
# 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
|
||||
profile_expand_remote() { # "@name" -> user@host, otherwise pass through
|
||||
local in="$1"
|
||||
if [[ "$in" == @* ]]; then
|
||||
@@ -75,6 +143,13 @@ profile_expand_remote() { # "@name" -> user@host, otherwise pass through
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
# 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.
|
||||
profile_apply_defaults() { # set globals if unset; named overrides default
|
||||
local name="$1" v
|
||||
# defaults
|
||||
@@ -96,6 +171,7 @@ profile_apply_defaults() { # set globals if unset; named overrides default
|
||||
|
||||
# ----------------------------
|
||||
# Defaults
|
||||
# Purpose: initialize built-in defaults before applying profiles and flags.
|
||||
# ----------------------------
|
||||
TUNNEL_PORT=2222 # remote-side port exposed via -R
|
||||
LOCAL_SSH_PORT=22 # local sshd port to forward to
|
||||
@@ -147,6 +223,7 @@ EOF
|
||||
|
||||
# ----------------------------
|
||||
# Positional parsing
|
||||
# Purpose: enforce command grammar and capture the five required positionals.
|
||||
# ----------------------------
|
||||
[[ $# -lt 5 ]] && usage
|
||||
|
||||
@@ -171,6 +248,7 @@ REMOTE="$(profile_expand_remote "$REMOTE")"
|
||||
|
||||
# ----------------------------
|
||||
# Optional flags
|
||||
# Purpose: parse options and override derived defaults.
|
||||
# ----------------------------
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -224,6 +302,7 @@ done
|
||||
|
||||
# ----------------------------
|
||||
# Duration validation
|
||||
# Purpose: accept forms like 30m, 2h, 1d (s/m/h/d).
|
||||
# ----------------------------
|
||||
if [[ ! "$DURATION" =~ ^[0-9]+[smhd]$ ]]; then
|
||||
echo "Invalid duration '$DURATION' (use forms like 30m, 2h, 1d)." >&2
|
||||
@@ -232,6 +311,7 @@ fi
|
||||
|
||||
# ----------------------------
|
||||
# Split remote user/host
|
||||
# Purpose: support user:host or user@host; reject anything else (except @profile pre-expanded).
|
||||
# ----------------------------
|
||||
REMOTE_USER="" REMOTE_HOST=""
|
||||
if [[ "$REMOTE" == *:* ]]; then
|
||||
@@ -247,6 +327,7 @@ fi
|
||||
|
||||
# ----------------------------
|
||||
# Deps check
|
||||
# Purpose: fail fast if mandatory tools are missing.
|
||||
# ----------------------------
|
||||
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; }
|
||||
@@ -254,7 +335,13 @@ command -v tail >/dev/null 2>&1 || { echo "tail not found."; exit 1; }
|
||||
|
||||
# ----------------------------
|
||||
# Accessor temporary authorization
|
||||
# Purpose: optionally add and later remove a restricted authorized_keys entry.
|
||||
# ----------------------------
|
||||
# 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
|
||||
restrict_key_line() { # usage: restrict_key_line <pubkey_text>
|
||||
local pk="$1"
|
||||
# Normalize
|
||||
@@ -267,6 +354,12 @@ restrict_key_line() { # usage: restrict_key_line <pubkey_text>
|
||||
printf 'from="127.0.0.1",command="internal-sftp",restrict %s' "$pk"
|
||||
}
|
||||
|
||||
# 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
|
||||
add_temp_authorized_key() {
|
||||
local pubkey_text="$1"
|
||||
local ak="$HOME/.ssh/authorized_keys"
|
||||
@@ -295,6 +388,11 @@ add_temp_authorized_key() {
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
# 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.
|
||||
remove_temp_authorized_key() {
|
||||
local ak="$HOME/.ssh/authorized_keys"
|
||||
[[ -z "${ADDED_MARKER_ID:-}" ]] && return 0
|
||||
@@ -333,6 +431,7 @@ fi
|
||||
|
||||
# ----------------------------
|
||||
# Banner
|
||||
# Purpose: inform the user what will happen and where to connect from the remote.
|
||||
# ----------------------------
|
||||
echo "⏳ Sharing '${FOLDER}' via reverse SSH:"
|
||||
echo " local sshd port : ${LOCAL_SSH_PORT}"
|
||||
@@ -346,7 +445,9 @@ echo
|
||||
|
||||
# ----------------------------
|
||||
# Invite (optional)
|
||||
# If accessor key was pre-authorized, we can omit the auth-setup line.
|
||||
# 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.
|
||||
# ----------------------------
|
||||
if $INVITE; then
|
||||
INVITE_CMD="backtunnel-access '${FOLDER}' from ${REMOTE_USER}@${REMOTE_HOST} -p ${TUNNEL_PORT} -m '${INVITE_MOUNT}'"
|
||||
@@ -409,6 +510,7 @@ echo "To stop sharing early: press Ctrl+C in this window."
|
||||
|
||||
# ----------------------------
|
||||
# Pre-flight: warn if remote loopback port already in use (best-effort)
|
||||
# Purpose: give an actionable warning before attempting the -R bind.
|
||||
# ----------------------------
|
||||
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
|
||||
@@ -418,10 +520,14 @@ fi
|
||||
|
||||
# ----------------------------
|
||||
# Cleanup & run SSH
|
||||
# Purpose: start the tunnel, wait bounded by duration, and guarantee cleanup via trap.
|
||||
# ----------------------------
|
||||
SSH_PID=""
|
||||
|
||||
# shellcheck disable=SC2317
|
||||
# cleanup: stop the background ssh process and remove any temporary authorized key.
|
||||
# Notes:
|
||||
# - Invoked on INT/TERM/EXIT to ensure resources are released.
|
||||
cleanup() {
|
||||
# stop ssh child if running
|
||||
if [[ -n "${SSH_PID:-}" ]] && kill -0 "$SSH_PID" 2>/dev/null; then
|
||||
@@ -444,7 +550,7 @@ ssh -N \
|
||||
SSH_PID=$!
|
||||
|
||||
# Wait for ssh to exit, but bounded by duration:
|
||||
# Use 'timeout … tail --pid=PID -f /dev/null' so we don't need bash -c 'wait "$1"'
|
||||
# Rationale: use 'timeout … tail --pid=PID -f /dev/null' to avoid subshell/wait-loop complexity.
|
||||
if timeout "$DURATION" tail --pid="$SSH_PID" -f /dev/null; then
|
||||
# ssh exited on its own before timeout
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user