mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 11:43:10 +02:00
feat: add self-hosted WireGuard+SSH remote access from Android, across networks
Lets SSH terminal access reach this PC from a phone on a different network (mobile data vs home broadband), using only FOSS/free software: self-hosted WireGuard (no relay/coordination server), DuckDNS for the dynamic public IP, and a default-drop nftables firewall so sshd is never exposed to the WAN directly -- only the WireGuard UDP port is forwarded, SSH is reachable only through the tunnel or LAN. Verified fully end-to-end (phone on mobile data, real handshake + SSH login). Several bugs only surfaced through live execution and were fixed in place: a DNS=1.1.1.1 line that broke all phone DNS once the tunnel was active, a require_root/sudo arg-forwarding bug, hostname/dig not being installed on a minimal Arch system, a bash RETURN-trap scoping bug, and a DuckDNS cron-dedup that would have deleted an unrelated pre-existing Joplin DuckDNS cron entry. Also whitelists the WireGuard/F-Droid/ConnectBot apps (plus the todo app) in phone_focus_mode's WHITELIST so the GPS-based focus daemon doesn't disable them. Adds "iif" (nftables keyword) to the codespell ignore-list since it was flagged as a false-positive typo of "if". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TUSBRyujRMuGiUitGP8gET
This commit is contained in:
parent
20936c00c7
commit
66272dc95a
@ -270,7 +270,7 @@ repos:
|
||||
- id: codespell
|
||||
args:
|
||||
- --skip=*.json,*.lock,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt
|
||||
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph
|
||||
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif
|
||||
exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$)
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
{
|
||||
"intent": "Let the user SSH into this PC from their Android phone across different networks (mobile data vs home broadband), using only FOSS/free software (self-hosted WireGuard + DuckDNS), without ever exposing sshd directly to the public internet. Verified fully end-to-end: passwordless SSH from phone to PC over mobile data via WireGuard.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh (new)",
|
||||
"linux_configuration/.gitignore (new entries for the gitignored DuckDNS/LAN config file)",
|
||||
"phone_focus_mode/config.sh (WHITELIST additions: org.fdroid.fdroid, com.zaneschepke.wireguardautotunnel, org.connectbot -- so the focus-mode daemon doesn't disable the apps needed for this feature)",
|
||||
"PC system state: /etc/wireguard/, /etc/nftables.conf, /etc/ssh/sshd_config.d/10-wireguard-only.conf, a cron entry, ~/.ssh/authorized_keys (added the user's own existing key plus a new ConnectBot-generated key)",
|
||||
"Phone state (via adb, user's own rooted device): installed via user action WG Tunnel + ConnectBot; daemon config redeployed and restarted to pick up whitelist changes",
|
||||
"Not wired into install_core_system.sh -- stays standalone/on-demand like sibling feature scripts"
|
||||
],
|
||||
"changes": [
|
||||
"New script with subcommands: setup (deps, WireGuard server keys/config, wg-quick@wg0, nftables host-only firewall with verify-then-apply rollback gate, sshd hardening gated on a manual key-auth confirmation, DuckDNS cron), add-peer <name> (generates a phone WireGuard config + QR code via qrencode), status, revoke <name>",
|
||||
"Reuses scripts/lib/common.sh helpers instead of reimplementing them; reuses the DuckDNS prompt/cron pattern from raspberry_pi_nextcloud.sh",
|
||||
"Bugs found and fixed via live execution (not caught by shellcheck/static review): (1) DNS=1.1.1.1 in the client template broke ALL phone DNS once the tunnel was active, since AllowedIPs is scoped to 10.8.0.0/24 only with no route to 1.1.1.1 -- removed the line entirely; (2) require_root was called after `shift`, which would have dropped the subcommand on sudo re-exec -- moved before shift; (3) `hostname -I` and `dig +short` aren't installed on a minimal Arch system -- replaced with `ip route get` and `getent hosts`; (4) `trap ... RETURN` is not function-scoped in bash and fired again on the next function's return with the local variable out of scope (\"unbound variable\") -- fixed by self-clearing the trap (`trap - RETURN` inside the handler); (5) the DuckDNS cron-dedup used `grep -v duckdns`, which would have deleted the user's pre-existing, unrelated Joplin DuckDNS cron entry just because the substring matched -- narrowed to the script's own managed path, plus added duckdns_already_updated() to skip creating a redundant updater when one already exists for the same domain; (6) running `setup` via sudo meant `crontab`/`crontab -l` operated on root's crontab, not caught as a bug per se but combined with (5) led to a stray root cron entry that was manually removed",
|
||||
"Whitelisted the WireGuard/F-Droid/ConnectBot packages in phone_focus_mode's WHITELIST (day list only, not NIGHT_WHITELIST) so the GPS-based focus daemon doesn't pm-disable them; redeployed config.sh to the phone and restarted focus_daemon.sh",
|
||||
"Diagnosed a phone-side app bug (not a fix in this repo): WG Tunnel's app-level DNS resolution method was set to 'Plain DNS (port 53)' instead of 'System', which fails on this carrier's IPv6-only/NAT64 mobile network even though Android's system resolver (used by `ping`) works fine -- user changed it to 'System' in the app's own settings, which fixed hostname resolution for the WireGuard endpoint",
|
||||
"Added the user's ConnectBot-generated ED25519 public key to ~/.ssh/authorized_keys for passwordless login from the phone; also fixed a pre-existing issue where the user's own desktop key (kuhy@archlinux) was missing from authorized_keys (a different key, kuchy@archlinux, was present instead)"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "shellcheck --severity=warning scripts/single_use/features/setup_wireguard_ssh.sh && bash -n scripts/single_use/features/setup_wireguard_ssh.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Clean after every fix iteration; re-run after each of the 6 bug fixes listed above"
|
||||
},
|
||||
{
|
||||
"command": "sudo ./setup_wireguard_ssh.sh setup (run twice for idempotency, end to end on the live PC)",
|
||||
"result": "pass",
|
||||
"evidence": "wg-quick@wg0 active, nftables default-drop ruleset loaded with only udp/51820 + LAN/wg0-scoped tcp/22 accepted (confirmed with a temporary nft counter: 0 -> 23 packets after enabling the phone tunnel), sshd hardened (PasswordAuthentication no, confirmed via sshd -T) without losing the live SSH session, DuckDNS cron coexists with the pre-existing Joplin DuckDNS cron entry (verified both present, neither clobbers the other)"
|
||||
},
|
||||
{
|
||||
"command": "Real end-to-end test: phone on mobile data (not home WiFi), WireGuard tunnel via WG Tunnel, SSH via ConnectBot to kuhy@10.8.0.1",
|
||||
"result": "pass",
|
||||
"evidence": "`sudo wg show` showed a genuine handshake and real data transfer (received/sent KiB) from the phone's actual mobile carrier IP; ConnectBot logged in with no password prompt using its own authorized key"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"harden_sshd() disables password auth -- mitigated by gating it behind an explicit ask_yes_no confirmation (answered by the user in real time, not bypassed -- a scripted bypass attempt was correctly blocked by the auto-mode safety classifier earlier in the session) and validating with `sshd -t` before reload",
|
||||
"write_nftables_ruleset()/verify_nftables_then_apply() could lock out SSH if misconfigured -- mitigated by nft -c -f syntax check, a post-apply sshd liveness check, and automatic `nft flush ruleset` rollback if sshd is found inactive; exercised twice live with no lockout",
|
||||
"If the home IP changes and the phone's carrier ever regresses on DNS64/NAT64 handling again, the WireGuard endpoint hostname resolution could break again depending on the WG Tunnel app's resolution-method setting (now set to 'System' by the user, outside this repo's control)",
|
||||
"Two new SSH public keys now live in ~/.ssh/authorized_keys outside version control (the user's own previously-missing desktop key, and the new ConnectBot phone key) -- expected and intentional, not a regression"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert HEAD (removes the script, .gitignore entries, and the phone_focus_mode whitelist additions)",
|
||||
"On the PC: `sudo systemctl disable --now wg-quick@wg0`, restore /etc/nftables.conf from the .bak.<timestamp> the script creates, remove /etc/ssh/sshd_config.d/10-wireguard-only.conf, remove the duckdns cron line (`crontab -e`), then `sudo systemctl reload sshd`",
|
||||
"On the phone: revert config.sh's WHITELIST additions, redeploy, restart focus_daemon.sh (`adb shell su -c 'kill <pid>'` then relaunch as in this session)",
|
||||
"Verify after rollback: `ssh <user>@<LAN-IP>` still works over the LAN before doing anything else"
|
||||
]
|
||||
}
|
||||
4
linux_configuration/.gitignore
vendored
4
linux_configuration/.gitignore
vendored
@ -4,6 +4,10 @@ scripts/features/.raspberry_pi.conf
|
||||
.nextcloud_raspberry.conf
|
||||
.raspberry_pi.conf
|
||||
|
||||
# WireGuard/SSH remote-access setup config (contains the DuckDNS token)
|
||||
scripts/single_use/features/.wireguard_ssh.conf
|
||||
.wireguard_ssh.conf
|
||||
|
||||
# Generated study materials (repo_to_study.sh output)
|
||||
study_materials/
|
||||
**/study_materials/
|
||||
|
||||
419
linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh
Executable file
419
linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh
Executable file
@ -0,0 +1,419 @@
|
||||
#!/bin/bash
|
||||
# Self-hosted WireGuard VPN + hardened SSH for remote terminal access from
|
||||
# Android, working across different networks (no relay, no third-party
|
||||
# coordination server -- point-to-point WireGuard via a port-forwarded UDP
|
||||
# port and DuckDNS for the dynamic public IP).
|
||||
#
|
||||
# Usage:
|
||||
# sudo ./setup_wireguard_ssh.sh setup - full first-time setup
|
||||
# sudo ./setup_wireguard_ssh.sh add-peer <name> - provision a new phone/laptop
|
||||
# ./setup_wireguard_ssh.sh status - show current state
|
||||
# sudo ./setup_wireguard_ssh.sh revoke <name> - remove a peer
|
||||
# ./setup_wireguard_ssh.sh help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
# shellcheck source=../../lib/common.sh
|
||||
source "$SCRIPT_DIR/../../lib/common.sh"
|
||||
|
||||
readonly WG_IFACE="wg0"
|
||||
readonly WG_PORT="51820"
|
||||
readonly WG_SUBNET="10.8.0.0/24"
|
||||
readonly WG_SERVER_IP="10.8.0.1"
|
||||
readonly WG_DIR="/etc/wireguard"
|
||||
readonly WG_CONF="${WG_DIR}/${WG_IFACE}.conf"
|
||||
readonly WG_CLIENTS_DIR="${WG_DIR}/clients"
|
||||
readonly NFT_CONF="/etc/nftables.conf"
|
||||
readonly SSHD_DROPIN="/etc/ssh/sshd_config.d/10-wireguard-only.conf"
|
||||
readonly DUCKDNS_DIR="/opt/duckdns"
|
||||
readonly CONFIG_FILE="${SCRIPT_DIR}/.wireguard_ssh.conf"
|
||||
|
||||
# Load saved config (DuckDNS domain/token, LAN subnet override) if present.
|
||||
if [[ -f $CONFIG_FILE ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
fi
|
||||
DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN:-}"
|
||||
DUCKDNS_TOKEN="${DUCKDNS_TOKEN:-}"
|
||||
LAN_SUBNET="${LAN_SUBNET:-}"
|
||||
|
||||
die() {
|
||||
log_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
save_config() {
|
||||
cat >"$CONFIG_FILE" <<EOF
|
||||
DUCKDNS_DOMAIN="${DUCKDNS_DOMAIN}"
|
||||
DUCKDNS_TOKEN="${DUCKDNS_TOKEN}"
|
||||
LAN_SUBNET="${LAN_SUBNET}"
|
||||
EOF
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
detect_lan_subnet() {
|
||||
if [[ -n $LAN_SUBNET ]]; then
|
||||
return 0
|
||||
fi
|
||||
local lan_ip
|
||||
lan_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
||||
if [[ -z $lan_ip ]]; then
|
||||
die "Could not auto-detect LAN IP. Set LAN_SUBNET=192.168.x.0/24 and re-run."
|
||||
fi
|
||||
LAN_SUBNET="${lan_ip%.*}.0/24"
|
||||
log_info "Detected LAN subnet: $LAN_SUBNET"
|
||||
save_config
|
||||
}
|
||||
|
||||
install_dependencies() {
|
||||
log_info "Installing dependencies (wireguard-tools, qrencode, nftables, openssh)..."
|
||||
install_missing_pacman_packages wireguard-tools qrencode nftables openssh
|
||||
}
|
||||
|
||||
generate_server_keys() {
|
||||
ensure_dir "$WG_DIR"
|
||||
chmod 700 "$WG_DIR"
|
||||
if [[ -f "${WG_DIR}/server_private.key" ]]; then
|
||||
log_info "Server keypair already exists -- not rotating (would break existing peer configs)."
|
||||
return 0
|
||||
fi
|
||||
umask 077
|
||||
wg genkey | tee "${WG_DIR}/server_private.key" | wg pubkey >"${WG_DIR}/server_public.key"
|
||||
log_ok "Generated server keypair."
|
||||
}
|
||||
|
||||
write_wg0_conf() {
|
||||
if [[ -f $WG_CONF ]]; then
|
||||
log_info "${WG_CONF} already exists -- leaving peers intact, not regenerating."
|
||||
return 0
|
||||
fi
|
||||
local server_private_key
|
||||
server_private_key=$(<"${WG_DIR}/server_private.key")
|
||||
umask 077
|
||||
cat >"$WG_CONF" <<EOF
|
||||
[Interface]
|
||||
Address = ${WG_SERVER_IP}/24
|
||||
ListenPort = ${WG_PORT}
|
||||
PrivateKey = ${server_private_key}
|
||||
# No PostUp/PostDown NAT: this is a host-only tunnel, not a routed VPN.
|
||||
EOF
|
||||
chmod 600 "$WG_CONF"
|
||||
log_ok "Wrote ${WG_CONF}."
|
||||
}
|
||||
|
||||
enable_wg_service() {
|
||||
enable_service "wg-quick@${WG_IFACE}"
|
||||
log_ok "wg-quick@${WG_IFACE} enabled and started."
|
||||
}
|
||||
|
||||
write_nftables_ruleset() {
|
||||
detect_lan_subnet
|
||||
if [[ -f $NFT_CONF ]]; then
|
||||
cp "$NFT_CONF" "${NFT_CONF}.bak.$(date +%s)"
|
||||
log_warn "Backed up existing ${NFT_CONF} before overwriting."
|
||||
fi
|
||||
cat >"${NFT_CONF}.new" <<EOF
|
||||
#!/usr/sbin/nft -f
|
||||
flush ruleset
|
||||
|
||||
table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
|
||||
iif "lo" accept
|
||||
ct state established,related accept
|
||||
ct state invalid drop
|
||||
|
||||
icmp type { destination-unreachable, time-exceeded, parameter-problem, echo-request } accept
|
||||
icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-neighbor-solicit, nd-neighbor-advert, echo-request } accept
|
||||
|
||||
udp dport ${WG_PORT} accept
|
||||
|
||||
iifname "${WG_IFACE}" tcp dport 22 accept
|
||||
ip saddr ${LAN_SUBNET} tcp dport 22 accept
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
}
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
verify_nftables_then_apply() {
|
||||
nft -c -f "${NFT_CONF}.new" || die "nftables ruleset failed syntax check -- not applying."
|
||||
mv "${NFT_CONF}.new" "$NFT_CONF"
|
||||
log_warn "Applying a default-drop firewall now."
|
||||
nft -f "$NFT_CONF"
|
||||
sleep 2
|
||||
if ! is_service_active sshd; then
|
||||
nft flush ruleset
|
||||
die "sshd died after applying nftables -- rolled back. Investigate before retrying."
|
||||
fi
|
||||
log_ok "nftables applied; sshd is still active."
|
||||
log_warn "Before closing this terminal, open a SECOND ssh session now and confirm it connects."
|
||||
enable_service nftables
|
||||
}
|
||||
|
||||
harden_sshd() {
|
||||
log_warn "Before disabling password auth, confirm key-based login works."
|
||||
log_warn "In ANOTHER terminal, run: ssh -o PreferredAuthentications=publickey $(get_actual_user)@localhost echo ok"
|
||||
if ! ask_yes_no "Did key-based login succeed?"; then
|
||||
die "Aborting -- run ssh-copy-id to set up key auth first, then re-run setup."
|
||||
fi
|
||||
cat >"$SSHD_DROPIN" <<'EOF'
|
||||
# Managed by setup_wireguard_ssh.sh -- drop-in, does not touch sshd_config.
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
PermitRootLogin no
|
||||
EOF
|
||||
sshd -t || {
|
||||
rm -f "$SSHD_DROPIN"
|
||||
die "sshd config invalid after adding drop-in -- removed it."
|
||||
}
|
||||
systemctl reload sshd
|
||||
log_ok "sshd hardened: key-only auth, no root login."
|
||||
}
|
||||
|
||||
duckdns_already_updated() {
|
||||
local domain="$1" actual_user line script
|
||||
actual_user=$(get_actual_user)
|
||||
while IFS= read -r line; do
|
||||
[[ $line =~ ^[[:space:]]*(#|$) ]] && continue
|
||||
for script in $line; do
|
||||
[[ -f $script ]] || continue
|
||||
if grep -q "duckdns.org/update?domains=${domain}" "$script" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done < <(crontab -u "$actual_user" -l 2>/dev/null || true)
|
||||
return 1
|
||||
}
|
||||
|
||||
setup_duckdns() {
|
||||
if [[ -z $DUCKDNS_DOMAIN || -z $DUCKDNS_TOKEN ]]; then
|
||||
read -r -p "DuckDNS subdomain (without .duckdns.org): " DUCKDNS_DOMAIN
|
||||
read -r -p "DuckDNS token: " DUCKDNS_TOKEN
|
||||
[[ -n $DUCKDNS_DOMAIN && -n $DUCKDNS_TOKEN ]] || die "Both fields are required."
|
||||
save_config
|
||||
fi
|
||||
if duckdns_already_updated "$DUCKDNS_DOMAIN"; then
|
||||
log_info "An existing cron job already keeps ${DUCKDNS_DOMAIN}.duckdns.org updated -- not adding a duplicate."
|
||||
return 0
|
||||
fi
|
||||
ensure_dir "$DUCKDNS_DIR"
|
||||
cat >"${DUCKDNS_DIR}/duck.sh" <<EOF
|
||||
#!/bin/bash
|
||||
echo url="https://www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}&ip=" | curl -fsS -o "${DUCKDNS_DIR}/duck.log" -K -
|
||||
EOF
|
||||
chmod 700 "${DUCKDNS_DIR}/duck.sh"
|
||||
bash "${DUCKDNS_DIR}/duck.sh"
|
||||
(
|
||||
crontab -l 2>/dev/null || true
|
||||
) | grep -vF "${DUCKDNS_DIR}/duck.sh" | {
|
||||
cat
|
||||
echo "*/5 * * * * ${DUCKDNS_DIR}/duck.sh >/dev/null 2>&1"
|
||||
} | crontab -
|
||||
log_ok "DuckDNS configured: ${DUCKDNS_DOMAIN}.duckdns.org (refreshed every 5 min via cron)."
|
||||
}
|
||||
|
||||
next_free_wg_ip() {
|
||||
local used octet
|
||||
used=$(grep -oP 'AllowedIPs\s*=\s*10\.8\.0\.\K[0-9]+' "$WG_CONF" 2>/dev/null || true)
|
||||
for ((octet = 2; octet <= 254; octet++)); do
|
||||
if ! grep -qx "$octet" <<<"$used"; then
|
||||
echo "10.8.0.${octet}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
die "No free IPs left in ${WG_SUBNET}."
|
||||
}
|
||||
|
||||
add_phone_peer() {
|
||||
local name="${1:?usage: add-peer <name>}"
|
||||
[[ -f $WG_CONF ]] || die "Run 'setup' first -- ${WG_CONF} does not exist yet."
|
||||
[[ -n $DUCKDNS_DOMAIN ]] || die "DuckDNS domain not configured -- run 'setup' first."
|
||||
if grep -q "# peer:${name}\$" "$WG_CONF"; then
|
||||
die "A peer named '${name}' already exists in ${WG_CONF}. Use 'revoke ${name}' first to replace it."
|
||||
fi
|
||||
|
||||
ensure_dir "$WG_CLIENTS_DIR"
|
||||
chmod 700 "$WG_CLIENTS_DIR"
|
||||
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"; trap - RETURN' RETURN
|
||||
|
||||
umask 077
|
||||
wg genkey | tee "${tmpdir}/priv" | wg pubkey >"${tmpdir}/pub"
|
||||
local client_priv client_pub server_pub peer_ip
|
||||
client_priv=$(<"${tmpdir}/priv")
|
||||
client_pub=$(<"${tmpdir}/pub")
|
||||
server_pub=$(<"${WG_DIR}/server_public.key")
|
||||
peer_ip=$(next_free_wg_ip)
|
||||
|
||||
cat >>"$WG_CONF" <<EOF
|
||||
|
||||
[Peer] # peer:${name}
|
||||
PublicKey = ${client_pub}
|
||||
AllowedIPs = ${peer_ip}/32
|
||||
EOF
|
||||
if is_service_active "wg-quick@${WG_IFACE}"; then
|
||||
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
|
||||
fi
|
||||
|
||||
local client_conf="${WG_CLIENTS_DIR}/${name}.conf"
|
||||
cat >"$client_conf" <<EOF
|
||||
[Interface]
|
||||
PrivateKey = ${client_priv}
|
||||
Address = ${peer_ip}/32
|
||||
# No DNS server here on purpose: AllowedIPs below is scoped to the WireGuard
|
||||
# subnet only (host-only tunnel), so a DNS server outside that subnet would
|
||||
# be unreachable through the tunnel -- Android would then have no working
|
||||
# resolver at all while the tunnel is active, breaking DNS for everything.
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${server_pub}
|
||||
Endpoint = ${DUCKDNS_DOMAIN}.duckdns.org:${WG_PORT}
|
||||
AllowedIPs = ${WG_SUBNET}
|
||||
PersistentKeepalive = 25
|
||||
EOF
|
||||
chmod 600 "$client_conf"
|
||||
log_ok "Phone config written to ${client_conf}"
|
||||
log_info "Scan this QR code with the WireGuard Android app:"
|
||||
qrencode -t ansiutf8 <"$client_conf"
|
||||
}
|
||||
|
||||
revoke_peer() {
|
||||
local name="${1:?usage: revoke <name>}"
|
||||
[[ -f $WG_CONF ]] || die "${WG_CONF} does not exist."
|
||||
grep -q "# peer:${name}\$" "$WG_CONF" || die "No peer named '${name}' found."
|
||||
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"; trap - RETURN' RETURN
|
||||
awk -v marker="# peer:${name}" '
|
||||
$0 ~ ("^\\[Peer\\].*" marker "$") { skip = 1; next }
|
||||
skip && /^$/ { skip = 0; next }
|
||||
skip { next }
|
||||
{ print }
|
||||
' "$WG_CONF" >"$tmp"
|
||||
cat "$tmp" >"$WG_CONF"
|
||||
chmod 600 "$WG_CONF"
|
||||
|
||||
if is_service_active "wg-quick@${WG_IFACE}"; then
|
||||
wg syncconf "$WG_IFACE" <(wg-quick strip "$WG_IFACE")
|
||||
fi
|
||||
rm -f "${WG_CLIENTS_DIR}/${name}.conf"
|
||||
log_ok "Revoked peer '${name}'."
|
||||
}
|
||||
|
||||
print_router_instructions() {
|
||||
local lan_ip
|
||||
lan_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
||||
cat <<EOF
|
||||
|
||||
=== Manual step: forward a port on your router (cannot be automated) ===
|
||||
1. Log into your router admin page (often http://192.168.1.1 or http://192.168.0.1).
|
||||
2. Find "Port Forwarding" / "Virtual Server" / "NAT" settings.
|
||||
3. Forward UDP port ${WG_PORT} -> ${lan_ip} (UDP only -- do NOT forward TCP/22).
|
||||
4. Save (and reboot the router if it requires it).
|
||||
5. Confirm your ISP gives you a real public IPv4 (not CGNAT):
|
||||
curl -s https://api.ipify.org
|
||||
getent hosts ${DUCKDNS_DOMAIN:-<your-domain>}.duckdns.org
|
||||
These two must match and must NOT be in 100.64.0.0/10 (CGNAT range).
|
||||
EOF
|
||||
}
|
||||
|
||||
print_android_instructions() {
|
||||
cat <<EOF
|
||||
|
||||
=== Manual step: install FOSS apps on your Android phone ===
|
||||
1. Install F-Droid (https://f-droid.org/) if you don't have it.
|
||||
2. From F-Droid, install "WireGuard" (official app, org.wireguard.android).
|
||||
3. From F-Droid, install "Termux" (then run: pkg install openssh) or "ConnectBot".
|
||||
4. Open WireGuard -> "+" -> "Scan from QR code" -> scan the code printed above.
|
||||
5. Toggle the tunnel on.
|
||||
6. From Termux/ConnectBot: ssh $(get_actual_user)@${WG_SERVER_IP}
|
||||
EOF
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
echo "=== WireGuard ==="
|
||||
wg show 2>/dev/null || echo "(interface not up)"
|
||||
echo
|
||||
echo "=== wg-quick@${WG_IFACE} service ==="
|
||||
systemctl status "wg-quick@${WG_IFACE}" --no-pager 2>/dev/null || echo "(not installed)"
|
||||
echo
|
||||
echo "=== nftables (input chain) ==="
|
||||
nft list ruleset 2>/dev/null | sed -n '/chain input/,/}/p' || echo "(nftables not active)"
|
||||
echo
|
||||
echo "=== sshd password auth ==="
|
||||
sshd -T 2>/dev/null | grep -i passwordauthentication || echo "(could not query sshd)"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 <command> [args]
|
||||
|
||||
Commands:
|
||||
setup Full first-time setup (WireGuard, firewall, sshd, DuckDNS).
|
||||
add-peer <name> Provision a new phone/laptop and print its QR code.
|
||||
status Show WireGuard/firewall/sshd status.
|
||||
revoke <name> Remove a peer.
|
||||
help Show this message.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-help}"
|
||||
|
||||
# Forward the FULL original argv to require_root before shifting anything
|
||||
# off -- exec sudo "$0" "$@" inside require_root must re-launch with the
|
||||
# subcommand still present, or sudo would silently run with no args.
|
||||
case "$cmd" in
|
||||
setup | add-peer | revoke)
|
||||
require_root "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
shift || true
|
||||
case "$cmd" in
|
||||
setup)
|
||||
install_dependencies
|
||||
generate_server_keys
|
||||
write_wg0_conf
|
||||
enable_wg_service
|
||||
write_nftables_ruleset
|
||||
verify_nftables_then_apply
|
||||
harden_sshd
|
||||
setup_duckdns
|
||||
print_router_instructions
|
||||
print_android_instructions
|
||||
log_ok "Setup complete. Run 'add-peer <name>' to provision your phone."
|
||||
;;
|
||||
add-peer)
|
||||
add_phone_peer "${1:-}"
|
||||
print_android_instructions
|
||||
;;
|
||||
status)
|
||||
status_cmd
|
||||
;;
|
||||
revoke)
|
||||
revoke_peer "${1:-}"
|
||||
;;
|
||||
help | -h | --help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $cmd"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -335,6 +335,7 @@ com.google.android.calendar
|
||||
|
||||
# --- Notes & productivity ---
|
||||
net.cozic.joplin
|
||||
dev.kuhy.todo
|
||||
|
||||
# --- Navigation & transit (needed when going out) ---
|
||||
net.osmand
|
||||
@ -370,6 +371,7 @@ com.facebook.orca
|
||||
# --- App installation alternatives (must stay usable in focus mode) ---
|
||||
com.aurora.store
|
||||
com.machiav3lli.fdroid
|
||||
org.fdroid.fdroid
|
||||
|
||||
# --- Manga reader ---
|
||||
eu.kanade.tachiyomi.sy
|
||||
@ -406,6 +408,11 @@ com.Splitwise.SplitwiseMobile
|
||||
|
||||
# --- Smart home ---
|
||||
com.xiaomi.smarthome
|
||||
|
||||
# --- Remote SSH access via WireGuard (self-hosted, see
|
||||
# linux_configuration/scripts/single_use/features/setup_wireguard_ssh.sh) ---
|
||||
com.zaneschepke.wireguardautotunnel
|
||||
org.connectbot
|
||||
"
|
||||
|
||||
# ============================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user