intial release
This commit is contained in:
parent
c2e0d0b71e
commit
db9f28e53d
3 changed files with 392 additions and 0 deletions
18
install.sh
Normal file
18
install.sh
Normal 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
357
pawdance
Normal file
|
|
@ -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 <<EOF
|
||||
$SCRIPT_NAME $VERSION – vpn wrapper over SSH
|
||||
|
||||
Sub‑commands:
|
||||
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
17
uninstall.sh
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue