diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2410ee3 --- /dev/null +++ b/Makefile @@ -0,0 +1,101 @@ +# BackTunnel convenience Makefile (for first-time users and simple installs) + +PREFIX ?= /usr +BINDIR := $(PREFIX)/bin +MANDIR := $(PREFIX)/share/man +KIO_SM := $(PREFIX)/share/kio/servicemenus +KSVC5 := $(PREFIX)/share/kservices5/ServiceMenus +APPDIR := $(PREFIX)/share/applications +BCOMP := $(PREFIX)/share/bash-completion/completions + +# Config paths for user init +XDG_CONFIG_HOME ?= $(HOME)/.config +BT_CFG_DIR := $(XDG_CONFIG_HOME)/backtunnel +BT_CFG_FILE := $(BT_CFG_DIR)/profiles.ini +BT_CFG_EXAMPLE := docs/profiles.ini.example + +.PHONY: all init install uninstall refresh man check shellcheck help + +all: help + +help: + @echo "BackTunnel Makefile" + @echo " make init # Copy example profiles.ini for current user (first run)" + @echo " make install # Install scripts, man page, service menus (root)" + @echo " make uninstall # Remove installed files (root)" + @echo " make refresh # Refresh KDE/desktop caches (root)" + @echo " make man # View man page locally (man ./man/backtunnel.1)" + @echo " make check # Basic bash syntax check" + @echo " make shellcheck # ShellCheck (if installed)" + +# --- First-time user init (no root needed) --- +init: + @mkdir -p "$(BT_CFG_DIR)" + @if [ -f "$(BT_CFG_FILE)" ]; then \ + echo "profiles.ini already exists at $(BT_CFG_FILE) — skipping."; \ + else \ + if [ -f "$(BT_CFG_EXAMPLE)" ]; then \ + cp "$(BT_CFG_EXAMPLE)" "$(BT_CFG_FILE)"; \ + echo "Created $(BT_CFG_FILE) from $(BT_CFG_EXAMPLE)."; \ + else \ + echo "Example file $(BT_CFG_EXAMPLE) not found."; exit 1; \ + fi \ + fi + +# --- Install/uninstall (root or DESTDIR) --- +install: + @install -Dm755 scripts/backtunnel-share "$(DESTDIR)$(BINDIR)/backtunnel-share" + @install -Dm755 scripts/backtunnel-access "$(DESTDIR)$(BINDIR)/backtunnel-access" + @install -Dm755 scripts/backtunnel-share-gui "$(DESTDIR)$(BINDIR)/backtunnel-share-gui" + @install -Dm644 man/backtunnel.1 "$(DESTDIR)$(MANDIR)/man1/backtunnel.1" + @install -Dm644 completions/backtunnel.bash "$(DESTDIR)$(BCOMP)/backtunnel-share" + @install -Dm644 completions/backtunnel.bash "$(DESTDIR)$(BCOMP)/backtunnel-access" + @install -Dm644 servicemenus/backtunnel_share.desktop "$(DESTDIR)$(KIO_SM)/backtunnel_share.desktop" + @install -Dm644 servicemenus/backtunnel_access.desktop "$(DESTDIR)$(KIO_SM)/backtunnel_access.desktop" + # Plasma 5 legacy path (harmless if unused) + @install -Dm644 servicemenus/backtunnel_share.desktop "$(DESTDIR)$(KSVC5)/backtunnel_share.desktop" + @install -Dm644 servicemenus/backtunnel_access.desktop "$(DESTDIR)$(KSVC5)/backtunnel_access.desktop" + # Optional desktop launcher if present + @if [ -f desktop/backtunnel.desktop ]; then \ + install -Dm644 desktop/backtunnel.desktop "$(DESTDIR)$(APPDIR)/backtunnel.desktop"; \ + fi + @$(MAKE) refresh + +uninstall: + @rm -f "$(DESTDIR)$(BINDIR)/backtunnel-share" \ + "$(DESTDIR)$(BINDIR)/backtunnel-access" \ + "$(DESTDIR)$(BINDIR)/backtunnel-share-gui" + @rm -f "$(DESTDIR)$(MANDIR)/man1/backtunnel.1" + @rm -f "$(DESTDIR)$(BCOMP)/backtunnel-share" \ + "$(DESTDIR)$(BCOMP)/backtunnel-access" + @rm -f "$(DESTDIR)$(KIO_SM)/backtunnel_share.desktop" \ + "$(DESTDIR)$(KIO_SM)/backtunnel_access.desktop" + @rm -f "$(DESTDIR)$(KSVC5)/backtunnel_share.desktop" \ + "$(DESTDIR)$(KSVC5)/backtunnel_access.desktop" + @rm -f "$(DESTDIR)$(APPDIR)/backtunnel.desktop" || true + @$(MAKE) refresh + +# --- Cache refreshers (no-op if tools missing) --- +refresh: + @command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q || true + @if command -v kbuildsycoca6 >/dev/null 2>&1; then \ + kbuildsycoca6 --noincremental >/dev/null 2>&1 || true; \ + elif command -v kbuildsycoca5 >/dev/null 2>&1; then \ + kbuildsycoca5 --noincremental >/dev/null 2>&1 || true; \ + fi + +# --- Dev helpers --- +man: + @man ./man/backtunnel.1 + +check: + @bash -n scripts/backtunnel-share + @bash -n scripts/backtunnel-access + @bash -n scripts/backtunnel-share-gui + @echo "bash -n OK." + +shellcheck: + @command -v shellcheck >/dev/null 2>&1 || { echo "ShellCheck not installed"; exit 1; } + @shellcheck scripts/backtunnel-share + @shellcheck scripts/backtunnel-access + @shellcheck scripts/backtunnel-share-gui diff --git a/README.md b/README.md index 94d3c94..8604c49 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,18 @@ backtunnel-access /path/to/folder from remoteuser:remotehost [options] --- +## 📁 Profiles (named remotes) + +BackTunnel supports **profiles** to simplify connections. Instead of typing +`user@host -p PORT -l PORT …` every time, you can define defaults and named remotes in: + +📖 Example config: see [docs/profiles.ini.example](docs/profiles.ini.example) + +System-wide default: /etc/backtunnel/profiles.ini (admins can edit) +Packaged example: /usr/share/backtunnel/profiles.ini + +--- + ## 🧰 Dolphin Service Menus Two context actions for Dolphin: diff --git a/docs/profiles.ini.example b/docs/profiles.ini.example new file mode 100644 index 0000000..a80eda1 --- /dev/null +++ b/docs/profiles.ini.example @@ -0,0 +1,13 @@ +[default] +duration=2h +tunnel_port=2222 +local_ssh_port=22 +invite_mount=~/remote-rssh +invite=true +qr=false + +[myvps] +user=me +host=vps.example.com +tunnel_port=4422 +invite_mount=~/remote-rssh diff --git a/man/backtunnel.1 b/man/backtunnel.1 index 8fcc213..43b3dad 100644 --- a/man/backtunnel.1 +++ b/man/backtunnel.1 @@ -1,51 +1,53 @@ .TH backtunnel 1 "September 2025" "1.2" "BackTunnel – Reverse SSH Sharing Toolkit" .SH NAME -backtunnel-share, backtunnel-access \- Secure reverse SSH folder sharing and access +backtunnel-share, backtunnel-access \- Secure reverse SSH folder sharing and access (with profiles) .SH SYNOPSIS .B backtunnel-share -/path/to/folder with remoteuser:remotehost for [options] +/path/to/folder with {remoteuser:remotehost|remoteuser@remotehost|@profilename} for [options] .B backtunnel-access -/path/to/folder from remoteuser:remotehost [options] +/path/to/folder from {remoteuser:remotehost|remoteuser@remotehost} [options] .SH DESCRIPTION -\fBbacktunnel-share\fR starts a reverse SSH tunnel from the local (sharing) machine to a remote, reachable host. -The tunnel exposes the local sshd (typically port 22) on a loopback port on the remote host using \fBssh -R\fR. -The sharing ends automatically after the given \fIduration\fR via \fBtimeout\fR. +\fBbacktunnel-share\fR starts a reverse SSH tunnel from the local (sharing) machine to a remote, +reachable host. The tunnel exposes the local sshd (typically port 22) on a loopback port on the +remote host using \fBssh -R\fR. Sharing ends automatically after the given \fIduration\fR via \fBtimeout\fR. +Profiles can be used to simplify the remote specification and default options. -With the \fB--invite\fR option, \fBbacktunnel-share\fR prints a ready-to-copy access command for the remote user, -which can be pasted directly into a chat or terminal. The invite can also be rendered as a QR code or written to a file. - -\fBbacktunnel-access\fR mounts the shared folder from the remote side using \fBsshfs\fR by connecting to \fBlocalhost:\fR on the remote host -(the port exposed by \fBbacktunnel-share\fR). +\fBbacktunnel-access\fR mounts the shared folder from the remote side using \fBsshfs\fR by connecting +to \fBlocalhost:\fR on the remote host (the port exposed by \fBbacktunnel-share\fR). .SH OPTIONS .SS backtunnel-share options .TP .B -p, --tunnel-port -Remote port to bind with \fB-R\fR (default: 2222). +Remote port to bind with \fB-R\fR (default: 2222). May be provided via profile. .TP .B -l, --local-ssh-port -Local sshd port to forward to (default: 22). +Local sshd port to forward to (default: 22). May be provided via profile. .TP .B -i, --invite -Print a ready-to-copy invite command for the remote user. +Print a ready-to-copy access command for the remote side. .TP .B --invite-mount -Mount point suggested in the invite (default: /mnt/remote-rssh). +Suggested mount point included in the invite text (default: /mnt/remote-rssh). May be provided via profile. .TP .B --invite-file -Also write the invite text (including unmount hint) to the given file. +Write the invite text (including unmount hint) to the given file. .TP .B --qr -Render the invite as a terminal QR code (requires \fBqrencode\fR). +Additionally render the invite as a QR code (requires \fBqrencode\fR). + +.TP +.B -h, --help +Show usage. .SS backtunnel-access options .TP @@ -65,9 +67,65 @@ Path to share (server) or to mount (client). .B remoteuser:remotehost Or \fBremoteuser@remotehost\fR. The remote host that accepts the initial SSH connection. +.TP +.B @profilename +A named profile that expands to \fBuser@host\fR and may also supply defaults for \fB--tunnel-port\fR, +\fB--local-ssh-port\fR, \fB--invite-mount\fR, and \fIduration\fR. See \fBPROFILES\fR. + .TP .B -Time to keep the share active. Accepts \fBn\fR\fIs\fR|\fIm\fR|\fIh\fR|\fId\fR forms, e.g. 30m, 2h, 1d. Passed to \fBtimeout\fR. +Time to keep the share active. Accepts \fBn\fR\fIs\fR|\fIm\fR|\fIh\fR|\fId\fR forms, e.g. 30m, 2h, 1d (passed to \fBtimeout\fR). +May be provided via the \fB[default]\fR profile; the positional \fI\fR takes precedence. + +.SH PROFILES +BackTunnel can read defaults and named remote definitions from (searched in order): +.P +\fB~/.config/backtunnel/profiles.ini\fR (per-user) +.br +\fB/etc/backtunnel/profiles.ini\fR (system-wide default) +.br +\fB/usr/share/backtunnel/profiles.ini\fR (packaged example/fallback) +.P +Two kinds of sections are recognized: + +.TP +.B [default] +Global defaults applied unless overridden by a named profile or CLI flags: +.IP +\fBduration\fR=2h +.br +\fBtunnel_port\fR=2222 +.br +\fBlocal_ssh_port\fR=22 +.br +\fBinvite_mount\fR=~/remote-rssh +.br +\fBinvite\fR=true|false +.br +\fBqr\fR=true|false + +.TP +.B [name] +A named profile providing at least \fBuser\fR and \fBhost\fR, and optional overrides: +.IP +\fBuser\fR=alice +.br +\fBhost\fR=vps.example.com +.br +\fBtunnel_port\fR=4422 +.br +\fBlocal_ssh_port\fR=22 +.br +\fBinvite_mount\fR=/mnt/remote-rssh + +.P +To use a profile, replace the remote with \fB@name\fR, e.g.: +.P +.nf +backtunnel-share /path/to/folder with @workvps for 1d +.fi +.P +Command-line options \fBalways override\fR values from profiles. .SH EXAMPLES .TP @@ -81,14 +139,9 @@ Share for 1 day, using custom ports: /home/user/docs with alice:vps.example.com for 1d -p 4422 -l 2222 .TP -Share for 2 hours and print an invite: +Share using a profile, then override port: .B backtunnel-share -/home/user/docs with alice@vps.example.com for 2h -i - -.TP -Share with QR invite: -.B backtunnel-share -/home/user/docs with alice@vps.example.com for 2h -i --qr +/home/user/docs with @workvps for 6h -p 5500 .TP Mount with default port and mount point: @@ -103,12 +156,21 @@ Mount with custom port and mount point: .SH NOTES By default, \fBssh -R\fR binds to 127.0.0.1 on the remote side, limiting access to local users on the remote machine. The client connects to \fBlocalhost:\fR from the remote host. +Ensure the sharing host's sshd provides a valid SFTP subsystem for sshfs. -The invite feature is intended for convenience: copy-paste the printed command into chat for the remote user. -Unmount with \fBfusermount -u \fR after use. +.SH FILES +.TP +\fB~/.config/backtunnel/profiles.ini\fR +Per-user configuration file containing defaults and named profiles. +.TP +\fB/etc/backtunnel/profiles.ini\fR +System-wide default profiles (editable by admins). +.TP +\fB/usr/share/backtunnel/profiles.ini\fR +Packaged example/fallback used when user/system config is absent. .SH SEE ALSO -ssh(1), sshfs(1), timeout(1), autossh(1), qrencode(1), fusermount(1) +ssh(1), sshfs(1), timeout(1), autossh(1), fusermount(1) .SH AUTHOR Matjaž Mozetič diff --git a/scripts/backtunnel-share b/scripts/backtunnel-share index f271287..23b1e00 100644 --- a/scripts/backtunnel-share +++ b/scripts/backtunnel-share @@ -10,6 +10,75 @@ set -euo pipefail +# Config search order: user → system → packaged example +CONFIG_USER="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini" +CONFIG_SYS="/etc/backtunnel/profiles.ini" +CONFIG_PKG="/usr/share/backtunnel/profiles.ini" + +# resolve CONFIG_FILE to first existing +if [[ -f "$CONFIG_USER" ]]; then + CONFIG_FILE="$CONFIG_USER" +elif [[ -f "$CONFIG_SYS" ]]; then + CONFIG_FILE="$CONFIG_SYS" +else + CONFIG_FILE="$CONFIG_PKG" +fi + +# shellcheck disable=SC2317 # invoked later at runtime; ShellCheck can't see call path +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 +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 +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" + v="$(ini_get default invite_mount)"; [[ -n "$v" && "${INVITE_MOUNT}" == "/mnt/remote-rssh" ]] && INVITE_MOUNT="$v" + 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 +} + + 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 @@ -61,6 +130,15 @@ shift 5 || true [[ "$KW1" != "with" ]] && usage [[ "$KW2" != "for" ]] && usage +# 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")" + # --- optional flags --- while [[ $# -gt 0 ]]; do case "$1" in diff --git a/scripts/backtunnel-share-gui b/scripts/backtunnel-share-gui index d8cafb1..cb4690c 100644 --- a/scripts/backtunnel-share-gui +++ b/scripts/backtunnel-share-gui @@ -1,45 +1,96 @@ #!/usr/bin/env bash - -# Copyright (c) 2025. LUXIM d.o.o., Slovenia - Matjaž Mozetič. - # GUI wrapper for BackTunnel "Share" action (Dolphin service menu) -# Prompts for parameters via kdialog and launches backtunnel-share in Konsole. +# Prompts for parameters via kdialog and launches backtunnel-share in a terminal. set -euo pipefail -# --- Selection from Dolphin --- +LOG="/tmp/backtunnel-share-gui.$UID.log" +exec > >(tee -a "$LOG") 2>&1 +export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" + FOLDER="${1:-}" if [[ -z "$FOLDER" ]]; then kdialog --error "No folder selected." || true + echo "No folder argument passed from service menu." >>"$LOG" exit 1 fi -# --- Defaults --- +# Defaults REMOTE_DEFAULT="user@vps.example.com" DURATION_DEFAULT="2h" TPORT_DEFAULT="2222" LPORT_DEFAULT="22" INVITE_MOUNT_DEFAULT="/mnt/remote-rssh" -# --- Prompts --- -REMOTE="$(kdialog --inputbox "Remote (user@host or user:host):" "$REMOTE_DEFAULT")" || exit 1 -DUR="$(kdialog --combobox "Share duration" 30m 2h 6h 1d 2d --editable "$DURATION_DEFAULT")" || exit 1 -TPORT="$(kdialog --inputbox "Tunnel port on remote:" "$TPORT_DEFAULT")" || exit 1 -LPORT="$(kdialog --inputbox "Local SSH port to expose:" "$LPORT_DEFAULT")" || exit 1 +# Profiles (search order: user -> system -> packaged fallback) +PROFILES_USER="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini" +PROFILES_SYS="/etc/backtunnel/profiles.ini" +PROFILES_PKG="/usr/share/backtunnel/profiles.ini" +if [[ -f "$PROFILES_USER" ]]; then PROFILES_FILE="$PROFILES_USER" +elif [[ -f "$PROFILES_SYS" ]]; then PROFILES_FILE="$PROFILES_SYS" +else PROFILES_FILE="$PROFILES_PKG" +fi -INV="" # explicit if-then; avoids SC2015 ("A && B || C is not if-then-else") +# If profiles exist, offer a profile picker and prefill dialogs +if [[ -f "$PROFILES_FILE" ]]; then + mapfile -t profs < <(awk '/^\[/{gsub(/^\[|\]$/,"",$1); if ($1!="default") print $1}' "$PROFILES_FILE") + if (( ${#profs[@]} )); then + choice="$(kdialog --combobox "Choose profile (or Cancel for manual)" "${profs[@]}")" || choice="" + if [[ -n "$choice" ]]; then + getval() { awk -v s="[""$choice""]" -v k="$1" ' + $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} + }' "$PROFILES_FILE" 2>/dev/null; } + u="$(getval user)"; h="$(getval host)" + if [[ -n "$u" && -n "$h" ]]; then + REMOTE_DEFAULT="$u@$h" + fi + v="$(getval tunnel_port)"; [[ -n "$v" ]] && TPORT_DEFAULT="$v" + v="$(getval local_ssh_port)"; [[ -n "$v" ]] && LPORT_DEFAULT="$v" + v="$(getval invite_mount)"; [[ -n "$v" ]] && INVITE_MOUNT_DEFAULT="$v" + # optional: default duration from [default] + vd="$(awk -v s="[default]" -v k="duration" ' + $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} + }' "$PROFILES_FILE" 2>/dev/null)" + [[ -n "$vd" ]] && DURATION_DEFAULT="$vd" + fi + fi +fi + +# Dialogs (Cancel returns 1; treat as benign exit) +REMOTE="$(kdialog --inputbox "Remote (user@host or user:host):" "$REMOTE_DEFAULT")" || exit 0 +DUR="$(kdialog --combobox "Share duration" 30m 2h 6h 1d 2d --editable "$DURATION_DEFAULT")" || exit 0 +TPORT="$(kdialog --inputbox "Tunnel port on remote:" "$TPORT_DEFAULT")" || exit 0 +LPORT="$(kdialog --inputbox "Local SSH port to expose:" "$LPORT_DEFAULT")" || exit 0 + +INV="" # set to "-i" if chosen if kdialog --yesno "Print invite line for chat?"; then INV="-i" fi -QR="" +QR="" # set to "--qr" if chosen if kdialog --yesno "Show QR code for the invite?"; then QR="--qr" fi -MP="$(kdialog --inputbox "Suggested mount point in invite:" "$INVITE_MOUNT_DEFAULT")" || exit 1 +MP="$(kdialog --inputbox "Suggested mount point in invite:" "$INVITE_MOUNT_DEFAULT")" || exit 0 -# --- Run share in Konsole (keeps session open) --- -exec konsole --noclose -e backtunnel-share \ - "$FOLDER" with "$REMOTE" for "$DUR" \ - -p "$TPORT" -l "$LPORT" $INV $QR --invite-mount "$MP" +# Build command safely as an array; append optional flags conditionally (SC2206-safe) +cmd=( backtunnel-share "$FOLDER" with "$REMOTE" for "$DUR" -p "$TPORT" -l "$LPORT" --invite-mount "$MP" ) +if [[ -n "$INV" ]]; then cmd+=("$INV"); fi +if [[ -n "$QR" ]]; then cmd+=("$QR"); fi + +if command -v konsole >/dev/null 2>&1; then + exec konsole --noclose -e "${cmd[@]}" +elif command -v xterm >/dev/null 2>&1; then + exec xterm -hold -e "${cmd[@]}" +else + nohup "${cmd[@]}" >>"$LOG" 2>&1 & + kdialog --msgbox "Sharing started in background.\nSee log: $LOG" + exit 0 +fi