diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..5f3ad00 --- /dev/null +++ b/install.sh @@ -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. diff --git a/pawdance b/pawdance new file mode 100644 index 0000000..f32e42b --- /dev/null +++ b/pawdance @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# pawdance.sh – EXACT client *and* server logic driven by an easy‑to‑edit config file +# ----------------------------------------------------------------------------- +# Modes (ROLE in config): +# client – brings up a point‑to‑point tunnel by SSH‑w’ing 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 hard‑coded 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}" + + # Private‑key 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 < Bring tunnel up (client or server, per ROLE). + down --config Tear tunnel down (runs cleanup logic). + make-config --role client|server -o + Generate commented example config to . + 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 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 diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..4a06a39 --- /dev/null +++ b/uninstall.sh @@ -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