intial release

This commit is contained in:
furrofurry 2025-07-25 12:45:57 +02:00
parent c2e0d0b71e
commit db9f28e53d
3 changed files with 392 additions and 0 deletions

18
install.sh Normal file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
SRC="${1:-./pawdance}"
DEST="/usr/local/bin/pawdance"
if [[ ! -f "$SRC" ]]; then
echo "Error: '$SRC' not found." >&2
exit 1
fi
echo "Installing pawdance to $DEST"ss
install -Dm755 "$SRC" "$DEST" # -D: create dirs as needed, -m755: chmod +x
echo "Done. Type 'pawdance --help' to verify."
echo syncing file system
sudo sync
echo done.

357
pawdance Normal file
View file

@ -0,0 +1,357 @@
#!/usr/bin/env bash
# pawdance.sh EXACT client *and* server logic driven by an easytoedit config file
# -----------------------------------------------------------------------------
# Modes (ROLE in config):
# client brings up a pointtopoint tunnel by SSHwing into the server.
# server creates the matching tunnel locally and (optionally) enables routing.
#
# Connection source (CONNECT_MODE):
# dns resolve REMOTE_HOST on every run.
# ip use the hardcoded REMOTE_CONNECT_IP4 and/or REMOTE_CONNECT_IP6.
# auto if REMOTE_HOST is set use dns, otherwise ip (default).
#
# Extra dial options:
# CONNECT_PREFER which address family to try first (auto|ipv4|ipv6).
# SSH_KEY_MODE set to "true" to pass an explicit private key via -i.
# SSH_KEY absolute path to that key (required if SSH_KEY_MODE=true).
#
# -----------------------------------------------------------------------------
# v4.5.1 better --help, commented example configs, SSH_KEY_MODE support.
# -----------------------------------------------------------------------------
set -euo pipefail
SCRIPT_NAME="pawdance"
VERSION="4.5.1"
CONFIG_FILE=""
SUBCMD=""
MAKECFG_OUT=""
SUDO=""
[[ $EUID -ne 0 ]] && SUDO="sudo"
log() { echo "[*] $*"; }
ok() { echo "[✔] $*"; }
err() { echo "[!] $*" >&2; exit 1; }
require_cmd() {
for c in "$@"; do command -v "$c" >/dev/null 2>&1 || err "Missing command: $c"; done
}
# ---------- globals (for cleanup) ----------
REMOTE_IP4_RESOLVED=""
REMOTE_IP6_RESOLVED=""
IPV4_GW=""; IFACE4=""
IPV6_GW=""; IFACE6=""
IPT_RULES_INSTALLED=false
# ---------------- Config ----------------
load_config() {
[[ -f "$CONFIG_FILE" ]] || err "Config not found: $CONFIG_FILE"
# shellcheck disable=SC1090
source "$CONFIG_FILE"
: "${ROLE:=client}" ; ROLE="${ROLE,,}"
: "${TUN_INDEX:=0}"
: "${TUN_DEV:=tun${TUN_INDEX}}"
: "${LOCAL_IP4:?LOCAL_IP4 must be set (CIDR)}"
: "${LOCAL_IP6:=}"
: "${MTU:=1500}"
case "$ROLE" in
client)
: "${CONNECT_MODE:=auto}" ; CONNECT_MODE="${CONNECT_MODE,,}"
: "${REMOTE_HOST:=}"
: "${REMOTE_CONNECT_IP4:=}"
: "${REMOTE_CONNECT_IP6:=}"
case "$CONNECT_MODE" in
dns) [[ -z "$REMOTE_HOST" ]] && err "CONNECT_MODE=dns requires REMOTE_HOST" ;;
ip) [[ -z "$REMOTE_CONNECT_IP4" && -z "$REMOTE_CONNECT_IP6" ]] && err "CONNECT_MODE=ip requires REMOTE_CONNECT_IP*" ;;
auto) : ;; # handled later
*) err "CONNECT_MODE must be dns, ip, or auto" ;;
esac
: "${REMOTE_USER:?REMOTE_USER must be set}"
: "${REMOTE_IP4:=}"
: "${REMOTE_IP6:=}"
# Crypto knobs with safe defaults
: "${SSH_KEX:=mlkem768x25519-sha256}"
: "${SSH_CIPHERS:=chacha20-poly1305@openssh.com}"
: "${SSH_MACS:=hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com}"
# Privatekey dial support
: "${SSH_KEY_MODE:=false}"
: "${SSH_KEY:=}"
[[ "$SSH_KEY_MODE" == "true" && -z "$SSH_KEY" ]] && err "SSH_KEY_MODE=true but SSH_KEY is empty"
# Routing toggles
: "${DEFAULT_ROUTE_IPV4:=true}"
: "${DEFAULT_ROUTE_IPV6:=true}"
: "${CONNECT_PREFER:=auto}" ;;
server)
: "${VPN_FORWARD:=true}"
: "${IP_FORWARD:=true}" ;;
*) err "Unknown ROLE '$ROLE' (client|server)" ;;
esac
}
# -------------- Example configs --------------
make_example_client_cfg() {
cat <<'EOF'
# ---------------------------------------------------------------------------
# pawdance
# ---------------------------------------------------------------------------
ROLE="client" # client or server
CONNECT_MODE="auto" # dns|ip|auto
REMOTE_HOST="vpn.example.com" # used when dns/auto
# REMOTE_CONNECT_IP4="203.0.113.42" # used when ip/auto with no REMOTE_HOST.
# REMOTE_CONNECT_IP6="2001:db8::42"
CONNECT_PREFER="auto" # auto|ipv4|ipv6
# --- SSH authentication -----------------------------------------------------
REMOTE_USER="youruser"
SSH_KEY_MODE="false" # true = pass explicit key; false = default chain
SSH_KEY="/home/alice/.ssh/id_ed25519" # only if SSH_KEY_MODE=true
# --- Tunnel parameters ------------------------------------------------------
TUN_INDEX="1"
TUN_DEV="tun${TUN_INDEX}"
LOCAL_IP4="10.0.1.2/24"
REMOTE_IP4="10.0.1.1"
LOCAL_IP6="2001:db8:1::2/64"
REMOTE_IP6="2001:db8:1::1"
MTU="1500"
# --- Crypto preferences -----------------------------------------
SSH_KEX="mlkem768x25519-sha256"
SSH_CIPHERS="chacha20-poly1305@openssh.com"
SSH_MACS="hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com"
# use remote server as vpn for all traffic.
# if set to false, vpn becomes transparent.
DEFAULT_ROUTE_IPV4="true"
DEFAULT_ROUTE_IPV6="true"
EOF
}
make_example_server_cfg() {
cat <<'EOF'
# ---------------------------------------------------------------------------
# pawdance
# ---------------------------------------------------------------------------
ROLE="server"
# --- Tunnel parameters ------------------------------------------------------
TUN_INDEX="1"
TUN_DEV="tun${TUN_INDEX}"
LOCAL_IP4="10.0.1.1/24"
LOCAL_IP6="2001:db8:1::1/64"
MTU="1500"
# allow clients to accsess networks on the server?
VPN_FORWARD="true" # iptables/ip6tables FORWARD rules
#keep this to true. It is required for the tunnel to work.
# this enables net.ipv4.ip_forward + net.ipv6.conf.all.forwarding
IP_FORWARD="true"
EOF
}
# -------------- Resolve remote (client) --------------
resolve_remote() {
case "$CONNECT_MODE" in
dns)
log "Resolving $REMOTE_HOST (dns)…"
REMOTE_IP4_RESOLVED=$(dig +short A "$REMOTE_HOST" | awk 'NF' | head -n1)
REMOTE_IP6_RESOLVED=$(dig +short AAAA "$REMOTE_HOST" | awk 'NF' | head -n1) ;;
ip)
REMOTE_IP4_RESOLVED="$REMOTE_CONNECT_IP4"
REMOTE_IP6_RESOLVED="$REMOTE_CONNECT_IP6" ;;
auto)
if [[ -n "$REMOTE_HOST" ]]; then
log "Resolving $REMOTE_HOST (auto)…"
REMOTE_IP4_RESOLVED=$(dig +short A "$REMOTE_HOST" | awk 'NF' | head -n1)
REMOTE_IP6_RESOLVED=$(dig +short AAAA "$REMOTE_HOST" | awk 'NF' | head -n1)
else
REMOTE_IP4_RESOLVED="$REMOTE_CONNECT_IP4"
REMOTE_IP6_RESOLVED="$REMOTE_CONNECT_IP6"
fi ;;
esac
[[ -z "$REMOTE_IP4_RESOLVED" && -z "$REMOTE_IP6_RESOLVED" ]] && err "No connect IP determined"
echo "[+] Connect IPv4: ${REMOTE_IP4_RESOLVED:-N/A}"
echo "[+] Connect IPv6: ${REMOTE_IP6_RESOLVED:-N/A}"
}
get_default_gws() {
IPV4_GW=$(ip route show default | awk '/default/ {print $3; exit}')
IFACE4=$(ip route show default | awk '/default/ {print $5; exit}')
IPV6_GW=$(ip -6 route show default | awk '/default/ {print $3; exit}')
IFACE6=$(ip -6 route show default | awk '/default/ {print $5; exit}')
}
# -------------- Cleanup (common) --------------
cleanup() {
log "Cleaning up…"
if [[ "$ROLE" == "client" ]]; then
$SUDO ip route del default via "$REMOTE_IP4" dev "$TUN_DEV" metric 1 2>/dev/null || true
$SUDO ip -6 route del default via "$REMOTE_IP6" dev "$TUN_DEV" metric 1 2>/dev/null || true
fi
$SUDO ip addr del "$LOCAL_IP4" dev "$TUN_DEV" 2>/dev/null || true
[[ -n "$LOCAL_IP6" ]] && $SUDO ip addr del "$LOCAL_IP6" dev "$TUN_DEV" 2>/dev/null || true
$SUDO ip link set "$TUN_DEV" down 2>/dev/null || true
$SUDO ip tuntap del dev "$TUN_DEV" mode tun 2>/dev/null || true
if [[ "$ROLE" == "client" ]]; then
[[ -n "$REMOTE_IP4_RESOLVED" ]] && \
$SUDO ip route del "$REMOTE_IP4_RESOLVED" via "$IPV4_GW" dev "$IFACE4" 2>/dev/null || true
[[ -n "$REMOTE_IP6_RESOLVED" ]] && \
$SUDO ip -6 route del "$REMOTE_IP6_RESOLVED" via "$IPV6_GW" dev "$IFACE6" 2>/dev/null || true
fi
if [[ "$ROLE" == "server" && "$VPN_FORWARD" == "true" && "$IPT_RULES_INSTALLED" == "true" ]]; then
$SUDO iptables -D FORWARD -i "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
$SUDO iptables -D FORWARD -o "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
$SUDO ip6tables -D FORWARD -i "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
$SUDO ip6tables -D FORWARD -o "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
fi
ok "Done."
exit 0
}
trap cleanup INT TERM
# -------------- Core logic --------------
client_up() {
require_cmd ip ssh dig
resolve_remote
get_default_gws
log "Creating $TUN_DEV…"
$SUDO ip tuntap add dev "$TUN_DEV" mode tun
$SUDO ip addr add "$LOCAL_IP4" dev "$TUN_DEV"
[[ -n "$LOCAL_IP6" ]] && $SUDO ip -6 addr add "$LOCAL_IP6" dev "$TUN_DEV"
$SUDO ip link set "$TUN_DEV" mtu "$MTU" up
log "Adding passthrough routes…"
[[ -n "$REMOTE_IP4_RESOLVED" && -n "$IPV4_GW" ]] && \
$SUDO ip route add "$REMOTE_IP4_RESOLVED" via "$IPV4_GW" dev "$IFACE4"
[[ -n "$REMOTE_IP6_RESOLVED" && -n "$IPV6_GW" ]] && \
$SUDO ip -6 route add "$REMOTE_IP6_RESOLVED" via "$IPV6_GW" dev "$IFACE6"
log "Setting default routes (metric 1)…"
[[ "$DEFAULT_ROUTE_IPV4" == "true" && -n "$REMOTE_IP4" ]] && \
$SUDO ip route add default via "$REMOTE_IP4" dev "$TUN_DEV" metric 1
[[ "$DEFAULT_ROUTE_IPV6" == "true" && -n "$REMOTE_IP6" ]] && \
$SUDO ip -6 route add default via "$REMOTE_IP6" dev "$TUN_DEV" metric 1
# Build SSH options
SSH_OPTS="-oKexAlgorithms=$SSH_KEX -oCiphers=$SSH_CIPHERS -oMACs=$SSH_MACS"
[[ "$SSH_KEY_MODE" == "true" ]] && SSH_OPTS="$SSH_OPTS -i $SSH_KEY"
# Choose IP to dial
case "$CONNECT_PREFER" in
ipv4) dial_ip="$REMOTE_IP4_RESOLVED" ;;
ipv6) dial_ip="$REMOTE_IP6_RESOLVED" ;;
auto)
dial_ip="${REMOTE_IP4_RESOLVED:-$REMOTE_IP6_RESOLVED}"
[[ -z "$dial_ip" ]] && dial_ip="$REMOTE_IP6_RESOLVED" ;;
*) err "CONNECT_PREFER must be auto|ipv4|ipv6" ;;
esac
log "Starting SSH tunnel to [$dial_ip] as $REMOTE_USER (tun$TUN_INDEX)…"
$SUDO ssh -T -w "$TUN_INDEX:$TUN_INDEX" $SSH_OPTS "$REMOTE_USER@$dial_ip"
ok "SSH exited. Cleaning up…"
cleanup
}
server_up() {
require_cmd ip
[[ "$VPN_FORWARD" == "true" ]] && require_cmd iptables ip6tables
log "Creating $TUN_DEV (server)…"
$SUDO ip tuntap add dev "$TUN_DEV" mode tun
$SUDO ip addr add "$LOCAL_IP4" dev "$TUN_DEV"
[[ -n "$LOCAL_IP6" ]] && $SUDO ip -6 addr add "$LOCAL_IP6" dev "$TUN_DEV"
$SUDO ip link set "$TUN_DEV" mtu "$MTU" up
if [[ "$IP_FORWARD" == "true" ]]; then
log "Enabling kernel IP forwarding…"
$SUDO sysctl -w net.ipv4.ip_forward=1
$SUDO sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [[ "$VPN_FORWARD" == "true" ]]; then
log "Installing FORWARD rules for $TUN_DEV…"
$SUDO iptables -A FORWARD -i "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
$SUDO iptables -A FORWARD -o "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
$SUDO ip6tables -A FORWARD -i "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
$SUDO ip6tables -A FORWARD -o "$TUN_DEV" -m conntrack --ctstate NEW,RELATED,ESTABLISHED -j ACCEPT
IPT_RULES_INSTALLED=true
fi
ok "Server tunnel up. Ctrl+C (or 'pawdance down') to clean up."
while true; do sleep 3600; done
}
# -------------- CLI --------------
usage() {
cat <<EOF
$SCRIPT_NAME $VERSION vpn wrapper over SSH
Subcommands:
up --config <file> Bring tunnel up (client or server, per ROLE).
down --config <file> Tear tunnel down (runs cleanup logic).
make-config --role client|server -o <file>
Generate commented example config to <file>.
help Show this help.
Example:
$SCRIPT_NAME make-config --role client -o ~/pawdance-client.conf
$SCRIPT_NAME up --config ~/pawdance-client.conf
EOF
}
[[ $# -lt 1 ]] && usage && exit 1
SUBCMD="$1"; shift
MAKECFG_ROLE="client"
while [[ $# -gt 0 ]]; do
case "$1" in
--config) CONFIG_FILE="$2"; shift 2 ;;
--role) MAKECFG_ROLE="$2"; shift 2 ;;
-o) MAKECFG_OUT="$2"; shift 2 ;;
-h|--help|help) usage; exit 0 ;;
*) shift ;;
esac
done
case "$SUBCMD" in
up)
load_config
[[ "$ROLE" == "server" ]] && server_up || client_up
;;
down)
load_config
cleanup
;;
make-config)
[[ -z "$MAKECFG_OUT" ]] && err "-o <file> required"
[[ "${MAKECFG_ROLE,,}" == "server" ]] && make_example_server_cfg >"$MAKECFG_OUT" \
|| make_example_client_cfg >"$MAKECFG_OUT"
ok "Config written to $MAKECFG_OUT"
;;
help|-h|--help) usage ;;
*) err "Unknown subcommand: $SUBCMD" ;;
esac

17
uninstall.sh Normal file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
DEST="/usr/local/bin/pawdance"
if [[ -f "$DEST" ]]; then
echo "Removing $DEST"
rm -f "$DEST"
echo "Uninstalled."
echo syncing file system...
sudo sync
echo done
else
echo "Nothing to do $DEST not found."
fi