From ae8ab9a7e0a21fce59ef685332b9ce883becbca3 Mon Sep 17 00:00:00 2001 From: sysadminmatmoz Date: Sun, 21 Sep 2025 18:56:15 +0200 Subject: [PATCH] Add `backtunnel-umount` script, host key policy support, and extend TUI/CLI integration 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. --- README.md | 266 ++++++++++++++++++++++++++++----- completions/backtunnel.bash | 28 +++- scripts/backtunnel-access | 15 +- scripts/backtunnel-access-tui | 13 +- scripts/backtunnel-open-term | 4 +- scripts/backtunnel-share | 14 +- scripts/backtunnel-umount | 35 +++++ tests/test-hostkey-policy.bats | 7 + tests/test-umount.bats | 11 ++ 9 files changed, 344 insertions(+), 49 deletions(-) create mode 100644 scripts/backtunnel-umount create mode 100644 tests/test-hostkey-policy.bats create mode 100644 tests/test-umount.bats diff --git a/README.md b/README.md index 144f12a..a63e93c 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,31 @@ No third-party relay, no cloud dependency – just peer-to-peer, temporary, SSH- ## ⚡ Quick Start ### 1. Accessor: Prepare your key (one-time) + ```bash backtunnel-keys print ``` + Send the printed public key to the sharer. ### 2. Sharer: Start sharing and authorize the accessor + ```bash backtunnel-share ~/Documents with alice@remotehost for 2h -i --allow-key ./alice.pub ``` + This injects a **temporary, restricted SFTP-only key** and prints an invite. ### 3. Accessor: Mount the shared folder + ```bash backtunnel-access '/home/sharer/Documents' from alice@remotehost -p 2222 -m ~/remote-rssh ``` + Unmount with: + ```bash -fusermount -u ~/remote-rssh +backtunnel-umount ~/remote-rssh ``` --- @@ -35,81 +42,230 @@ fusermount -u ~/remote-rssh - **NAT/firewall friendly**: works without port forwarding. - **Temporary by design**: shares auto-expire after a set duration. - **Invite workflow**: sharer sends a one-liner or QR code to accessor. -- **Restricted keys**: accessor keys are usable *only* for SFTP via the tunnel and are auto-removed when the share ends. -- **Profiles**: save defaults and common remotes in `profiles.ini`. -- **Desktop integration**: Dolphin (KDE) service menus for GUI sharing and access. +- **Restricted keys**: accessor keys are usable only for SFTP via the reverse tunnel and can be auto-added for the session. +- **Profiles**: save defaults and common remotes in profiles.ini. +- **Host key policy**: configurable StrictHostKeyChecking via BACKTUNNEL_HOSTKEY_POLICY. +- **Desktop integration**: Dolphin (KDE) service menus for GUI/TUI sharing and access. +- **Convenient TUIs/GUI**: guided prompts for sharer/accessor; terminal opener detects installed terminals. +- **Bash completion**: contextual completion for both CLI commands and TUIs. +- **Logs with rotation**: servicemenu invocations write logs and keep the last 10 entries. + +--- + +## 🧠 Roles and Flow + +- Sharer: exposes a local SSH service back to a remote host for a limited time. Runs backtunnel-share. +- Accessor: connects to the sharer via the remote host’s `127.0.0.1:PORT` and mounts a folder with sshfs. Runs backtunnel-access. +- Reverse tunnel: the sharer’s ssh -R binds `127.0.0.1:PORT` on the remote host and forwards back to the sharer’s sshd. + +--- + +## ✅ Requirements + +- Linux with Bash +- OpenSSH client (ssh, sftp) +- sshfs (FUSE) on the accessor machine +- Optional: + - qrencode (for QR code invites) + - A terminal emulator for service menu helpers (e.g., konsole, gnome-terminal, wezterm, kitty, etc.) --- ## 🔑 Commands -### Sharing -```bash -backtunnel-share /path/to/folder with user@host for [options] -``` -- `-p, --tunnel-port` Remote bind port (default: 2222) -- `-l, --local-ssh-port` Local sshd port to expose (default: 22) -- `-i, --invite` Print invite line -- `--qr` Show QR code (requires qrencode) -- `--allow-key FILE` Authorize a provided public key -- `--allow-known NAME` Authorize a stored key (~/.config/backtunnel/authorized/NAME.pub) +### Sharer side -### Accessing -```bash -backtunnel-access /path/to/folder from user@host [options] -``` -- `-p, --port` Tunnel port (default: 2222) -- `-m, --mount-point` Local mount point (default: ~/remote-rssh) +- backtunnel-share + - Start a time-bounded reverse SSH tunnel and optionally print an invite. + - Syntax: + ```bash + backtunnel-share /path/to/folder with user@host for [options] + ``` + - Duration: 10m, 30m, 1h, 2h, 6h, 12h, 1d, 2d (single unit e.g., 30m, 2h, 1d) + - Options: + - -p, --tunnel-port N: Remote bind port (default: 2222) + - -l, --local-ssh-port N: Local sshd port to forward to (default: 22) + - -i, --invite: Print a one-liner invite for the accessor + - --invite-mount PATH: Suggested mount point in the invite (default: $HOME/remote-rssh) + - --invite-file FILE: Save the invite text (with unmount hint) to a file + - --qr: Also show a QR code for the invite (requires qrencode) + - --allow-key FILE: Temporarily authorize an accessor public key for this session + - --allow-known NAME: Authorize a stored key (~/.config/backtunnel/authorized/NAME.pub) + - -h, --help: Show usage + +- backtunnel-share-tui + - Minimal TUI wrapper prompting for remote, duration, ports, and invite options, then runs backtunnel-share. + - Syntax: + ```bash + backtunnel-share-tui + ``` + +- backtunnel-authorize + - Store a named accessor public key for --allow-known usage. + - Syntax: + ```bash + backtunnel-authorize + ``` + - Stores at: `${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/authorized/.pub` + +### Accessor side + +- backtunnel-access + - Mount a folder over SFTP via the reverse tunnel’s `127.0.0.1:PORT`. + - Syntax: + ```bash + backtunnel-access /remote/folder from user@host [-p PORT] [-m MOUNTPOINT] + ``` + - Options: + - -p, --port N: Tunnel port on the remote host (default: 2222) + - -m, --mount-point PATH: Local mount point (default: $HOME/remote-rssh) + - -h, --help: Show usage + - Behavior: + - Checks mount point readiness and warns if not empty. + - Tries passwordless auth via a dedicated identity if present (~/.ssh/id_ed25519_backtunnel). + - Performs a quick SFTP visibility check of the target folder. + - Mounts with reconnect/keepalives. + +- backtunnel-access-tui + - Minimal TUI wrapper prompting for remote, tunnel port, remote folder, and mount point. + - Syntax: + ```bash + backtunnel-access-tui + ``` + +- backtunnel-keys + - Manage the accessor-side dedicated key pair (~/.ssh/id_ed25519_backtunnel). + - Syntax: + ```bash + backtunnel-keys print # emit public key (generate if missing) + backtunnel-keys path # show key paths + ``` + +- backtunnel-auth-setup + - Initialize a tunnel-only, SFTP-only authorized_keys entry on the remote (via the tunnel). + - Syntax: + ```bash + backtunnel-auth-setup [-p PORT] user@localhost + ``` + - Installs a restricted entry: + from="127.0.0.1",command="internal-sftp",restrict + +- backtunnel-umount + - Portable unmount helper (prefers fusermount3, then fusermount, then umount). + - Syntax: + + ```bash + backtunnel-umount + ``` + +- backtunnel-open-term + - Launch a command in an available terminal emulator; logs output with simple rotation. + - Automatically detects terminals (prefers konsole on KDE; supports wezterm, kitty, alacritty, gnome-terminal, kgx, tilix, xfce4-terminal, xterm). + - Used by service menus/GUI wrappers. --- -## 📁 Profiles +## 🔒 Security Model + +- Reverse tunnel bind: remote side binds on `127.0.0.1:PORT` (loopback only). +- Access is SFTP-based: + - Invite flow can be paired with a restricted authorized_keys entry: + - from="127.0.0.1",command="internal-sftp",restrict + - This limits usage to SFTP over the reverse tunnel only; no shell or port forwarding. +- Temporary authorization: + - Sharer can temporarily add an accessor public key for the session; it’s removed on exit. +- Host key policy: + - Controlled via the environment variable BACKTUNNEL_HOSTKEY_POLICY with values: + - accept-new (default), yes, no, ask + - Applied to ssh, sftp, and the ssh command used by sshfs. + +--- + +## 🧩 Profiles + +- Config locations (higher precedence first): + - `${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini` + - /etc/backtunnel/profiles.ini + - /usr/share/backtunnel/profiles.ini +- Example: -`~/.config/backtunnel/profiles.ini`: ```ini [default] tunnel_port=2222 -invite=true +local_ssh_port=22 invite_mount=$HOME/remote-rssh +invite=true +qr=false [work] user=alice host=vps.example.com tunnel_port=4422 ``` -Usage: -```bash -backtunnel-share ~/reports with @work for 6h -i --allow-known alice -``` + +- Usage with @profile: + ```bash + backtunnel-share ~/reports with @work for 6h -i --allow-known alice + ``` --- -## 🖥️ Dolphin Integration +## 🧭 Bash Completion -- **Share via BackTunnel…** – GUI dialog for sharer options. -- **Access via BackTunnel…** – GUI dialog for accessor options. - -Logs are written to: -``` -~/.local/state/backtunnel/servicemenu.*.log -``` +- Source completions/backtunnel.bash or install it via your system’s completion.d directory. +- Provides: + - Keyword scaffolding (with/from/for) + - @profile expansion + - Suggestions for durations and ports + - Key name/file completion for --allow-known/--allow-key + - Path completion for the first positional and mount path options +- Includes minimal completion for TUIs (first positional directory). --- -## 📦 Install +## 🖥️ Desktop Integration (KDE Dolphin) + +- Right-click in a folder: + - “Share via BackTunnel…” → opens a GUI/TUI flow for the sharer. + - “Access via BackTunnel (mount here)…” → opens a GUI/TUI flow for the accessor. +- Logs: + ``` + ~/.local/state/backtunnel/servicemenu.*.log + ``` + The launcher keeps the last 10 logs (simple rotation). + +--- + +## 🌐 Environment Variables + +- BACKTUNNEL_HOSTKEY_POLICY + - Controls StrictHostKeyChecking for ssh/sftp/sshfs: + - Values: accept-new (default), yes, no, ask + - Example: + ```bash + BACKTUNNEL_HOSTKEY_POLICY=strict # alias for 'yes' in many contexts + BACKTUNNEL_HOSTKEY_POLICY=accept-new + ``` + +--- + +## 🛠️ Installation ### From source + ```bash sudo bash scripts/install.sh make init # copy example profiles.ini ``` ### Arch Linux + ```bash makepkg -si ``` Uninstall: + ```bash sudo bash scripts/uninstall.sh # or with purge of defaults @@ -118,8 +274,44 @@ sudo PURGE=1 bash scripts/uninstall.sh --- -## 📖 Documentation +## 🔍 Troubleshooting + +- Port already in use on remote: + - backtunnel-share warns if `127.0.0.1:PORT` is busy; choose another with -p. +- Missing sshfs: + - Install sshfs on the accessor machine; ensure FUSE is available. +- SFTP path is not listable: + - backtunnel-access warns if SFTP cannot list the path. Verify folder existence and permissions. +- Passwordless auth isn't set: + - Try: + ```bash + backtunnel-auth-setup -p @localhost + ``` +- Unmount fails: + - Use the portable helper: + ```bash + backtunnel-umount + ``` + +--- + +## 🧪 Testing + +- Bats scaffolds are provided to extend: + - tests/test-umount.bats + - tests/test-hostkey-policy.bats + +Run tests (example): + ```bash +bats tests +``` + +--- + +## 📖 Man Page + +```bash man backtunnel ``` diff --git a/completions/backtunnel.bash b/completions/backtunnel.bash index b70739b..0e7f28e 100644 --- a/completions/backtunnel.bash +++ b/completions/backtunnel.bash @@ -241,4 +241,30 @@ _backtunnel_complete() { # Register for both commands complete -F _backtunnel_complete backtunnel-share -complete -F _backtunnel_complete backtunnel-access \ No newline at end of file +complete -F _backtunnel_complete backtunnel-access + +# Minimal TUI completers: complete only the first positional as a directory +_backtunnel_access_tui_complete() { + local cur + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ ${COMP_CWORD} -eq 1 ]]; then + compopt -o filenames 2>/dev/null + mapfile -t COMPREPLY < <(compgen -d -- "$cur") + else + COMPREPLY=() + fi +} +_backtunnel_share_tui_complete() { + local cur + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ ${COMP_CWORD} -eq 1 ]]; then + compopt -o filenames 2>/dev/null + mapfile -t COMPREPLY < <(compgen -d -- "$cur") + else + COMPREPLY=() + fi +} + +# Register TUI completion +complete -F _backtunnel_access_tui_complete backtunnel-access-tui +complete -F _backtunnel_share_tui_complete backtunnel-share-tui \ No newline at end of file diff --git a/scripts/backtunnel-access b/scripts/backtunnel-access index e9edef5..16fb1c0 100644 --- a/scripts/backtunnel-access +++ b/scripts/backtunnel-access @@ -41,6 +41,13 @@ 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 @@ -151,7 +158,7 @@ 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 \ +if ! ssh -o BatchMode=yes -o StrictHostKeyChecking="$HKP" \ -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" +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 @@ -190,4 +197,4 @@ sshfs \ -- "$REMOTE_USER@localhost:$FOLDER" "$MOUNTPOINT" echo "✅ Mounted at: $MOUNTPOINT" -echo "To unmount: fusermount -u \"$MOUNTPOINT\" || fusermount3 -u \"$MOUNTPOINT\"" +echo "To unmount: backtunnel-umount \"$MOUNTPOINT\"" diff --git a/scripts/backtunnel-access-tui b/scripts/backtunnel-access-tui index c0d8276..f172904 100644 --- a/scripts/backtunnel-access-tui +++ b/scripts/backtunnel-access-tui @@ -47,6 +47,13 @@ REMOTE="${REMOTE:-user@vps.example.com}" read -r -p "Tunnel port on remote [2222]: " PORT PORT="${PORT:-2222}" +# Ask for the remote folder that should be mounted (first positional of backtunnel-access) +read -r -p "Remote folder path to mount [~/]: " FOLDER +FOLDER="${FOLDER:-~/}" +if [[ "$FOLDER" == "~" ]]; then + FOLDER="~/" +fi + read -r -p "Mount point [${DEFAULT_MP}]: " MP MP="${MP:-$DEFAULT_MP}" @@ -62,7 +69,7 @@ if [[ ! -w "$MP" ]]; then fi echo -echo "Running: backtunnel-access '' from '$REMOTE' -p '$PORT' -m '$MP'" -echo "Note: you'll be prompted on the remote for the exact folder (as per your workflow)." +echo "Running: backtunnel-access '${FOLDER}' from '${REMOTE}' -p '${PORT}' -m '${MP}'" +echo "Note: the folder is accessed via SFTP on the remote through the reverse tunnel." # Replace this process with backtunnel-access (no return to this script after exec) -exec backtunnel-access "$MP" from "$REMOTE" -p "$PORT" -m "$MP" +exec backtunnel-access "$FOLDER" from "$REMOTE" -p "$PORT" -m "$MP" diff --git a/scripts/backtunnel-open-term b/scripts/backtunnel-open-term index 97daa28..059f31d 100644 --- a/scripts/backtunnel-open-term +++ b/scripts/backtunnel-open-term @@ -78,7 +78,8 @@ cmd=( "$@" ) if [[ -n "${KDE_FULL_SESSION:-}" ]] && command -v konsole >/dev/null 2>&1; then echo "konsole"; return fi - for t in kitty alacritty kgx gnome-terminal tilix xfce4-terminal konsole xterm; do + # Prefer widely used modern terminals first when not on KDE + for t in wezterm kitty alacritty kgx gnome-terminal tilix xfce4-terminal konsole xterm; do if command -v "$t" >/dev/null 2>&1; then echo "$t"; return; fi done echo "" # none @@ -101,6 +102,7 @@ cmd=( "$@" ) # Run command in terminal (use hold/noclose if supported) case "$term" in konsole) exec konsole --noclose -e "${cmd[@]}" ;; + wezterm) exec wezterm start -- "${cmd[@]}" ;; kitty) exec kitty -e "${cmd[@]}" ;; alacritty) exec alacritty -e "${cmd[@]}" ;; gnome-terminal) exec gnome-terminal -- bash -lc "exec $shell_cmd" ;; diff --git a/scripts/backtunnel-share b/scripts/backtunnel-share index 922985c..e3fb13c 100644 --- a/scripts/backtunnel-share +++ b/scripts/backtunnel-share @@ -75,6 +75,13 @@ set -euo pipefail +# 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 + # ---------------------------- # Config discovery # Purpose: choose the highest-precedence profiles.ini available. @@ -464,7 +471,7 @@ ${AUTH_CMD} ${INVITE_CMD} # Unmount when done: -fusermount -u '${INVITE_MOUNT}' || fusermount3 -u '${INVITE_MOUNT}' +backtunnel-umount '${INVITE_MOUNT}' EOT ) else @@ -476,7 +483,7 @@ EOT ${INVITE_CMD} # Unmount when done: -fusermount -u '${INVITE_MOUNT}' || fusermount3 -u '${INVITE_MOUNT}' +backtunnel-umount '${INVITE_MOUNT}' EOT ) fi @@ -512,7 +519,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}" \ +if ssh -o BatchMode=yes -o StrictHostKeyChecking="$HKP" -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 # You may 'exit 1' here if you prefer a hard failure @@ -545,6 +552,7 @@ ssh -N \ -o ExitOnForwardFailure=yes \ -o ServerAliveInterval=15 \ -o ServerAliveCountMax=3 \ + -o StrictHostKeyChecking="$HKP" \ -R "${TUNNEL_PORT}:localhost:${LOCAL_SSH_PORT}" \ -- "${REMOTE_USER}@${REMOTE_HOST}" & SSH_PID=$! diff --git a/scripts/backtunnel-umount b/scripts/backtunnel-umount new file mode 100644 index 0000000..c21caf1 --- /dev/null +++ b/scripts/backtunnel-umount @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: GPL-3.0-or-later +# Name: backtunnel-umount +# Summary: Unmount a BackTunnel FUSE mount point using the best available helper. +# Usage: +# backtunnel-umount +# Notes: +# - Prefers fusermount3, then fusermount; falls back to umount. +# - Expands a leading "~" in the mountpoint. +set -euo pipefail + +usage() { + echo "Usage: $(basename "$0") " >&2 + exit 1 +} + +case "${1:-}" in + -h|--help) usage ;; +esac + +MP="${1:-}" +[[ -n "$MP" ]] || usage + +# Expand leading ~ +if [[ "$MP" == "~"* ]]; then + MP="${MP/#\~/$HOME}" +fi + +if command -v fusermount3 >/dev/null 2>&1; then + exec fusermount3 -u -- "$MP" +elif command -v fusermount >/dev/null 2>&1; then + exec fusermount -u -- "$MP" +else + exec umount -- "$MP" +fi diff --git a/tests/test-hostkey-policy.bats b/tests/test-hostkey-policy.bats new file mode 100644 index 0000000..93553cc --- /dev/null +++ b/tests/test-hostkey-policy.bats @@ -0,0 +1,7 @@ +#!/usr/bin/env bats + +# Scaffold for host key policy; to be implemented with a controlled SSH target or dry-run hooks. + +@test "BACKTUNNEL_HOSTKEY_POLICY scaffold" { + skip "TODO: verify policy propagation into ssh/sftp/sshfs invocations" +} diff --git a/tests/test-umount.bats b/tests/test-umount.bats new file mode 100644 index 0000000..bdb59ad --- /dev/null +++ b/tests/test-umount.bats @@ -0,0 +1,11 @@ +#!/usr/bin/env bats + +@test "backtunnel-umount shows usage with no args" { + run scripts/backtunnel-umount + [ "$status" -ne 0 ] + [[ "$output" == *"Usage:"* ]] +} + +@test "backtunnel-umount integration (scaffold)" { + skip "TODO: create a temporary FUSE mount and verify unmount behavior" +}