Skip to content

Commit af8a549

Browse files
committed
Harden DNS safety: never leave server with broken DNS or locked out
- Add global EXIT trap to auto-fix DNS if script crashes mid-operation - Back up resolv.conf before setup, back up sshd_config before sshtun-user - Validate sshd_config with sshd -t after modification, rollback if broken - Disable systemd-resolved in hard-stop fallback (prevents reboot reclaim) - Fix preflight DNS: overwrite dead stub instead of appending - Fix uninstall DNS check: use getent+curl instead of host/nslookup - Verify DNS works after disabling stub listener, write fallback if broken - Fix IP detection: try 3 services, auto-fix broken resolv.conf first
1 parent b0d92be commit af8a549

1 file changed

Lines changed: 130 additions & 8 deletions

File tree

dnstm-setup.sh

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ set -euo pipefail
1313
VERSION="1.3"
1414
TOTAL_STEPS=12
1515

16+
# ─── Safety: ensure DNS is never left broken on exit ──────────────────────────
17+
18+
_dnstm_cleanup_dns() {
19+
# If resolv.conf is empty, missing, or points only at a dead stub, fix it.
20+
if [[ ! -s /etc/resolv.conf ]] || \
21+
( grep -q '127\.0\.0\.53' /etc/resolv.conf 2>/dev/null && \
22+
! ss -ulnp 2>/dev/null | grep -q '127\.0\.0\.53.*systemd-resolve' ); then
23+
chattr -i /etc/resolv.conf 2>/dev/null || true
24+
cat > /etc/resolv.conf 2>/dev/null <<'DNSEOF' || true
25+
nameserver 8.8.8.8
26+
nameserver 1.1.1.1
27+
DNSEOF
28+
fi
29+
}
30+
trap '_dnstm_cleanup_dns' EXIT
31+
1632
# ─── Colors & Formatting ───────────────────────────────────────────────────────
1733

1834
RED='\033[0;31m'
@@ -1067,14 +1083,20 @@ ensure_resolv_conf_fallback() {
10671083
# After stopping systemd-resolved, /etc/resolv.conf may still point to
10681084
# 127.0.0.53 which is now dead. Write a temporary fallback so the script
10691085
# can still resolve hostnames (e.g. github.com for downloads).
1070-
if grep -q '127\.0\.0\.53' /etc/resolv.conf 2>/dev/null; then
1086+
if grep -q '127\.0\.0\.53' /etc/resolv.conf 2>/dev/null || \
1087+
[[ ! -s /etc/resolv.conf ]]; then
10711088
print_info "Updating /etc/resolv.conf with public DNS fallback"
10721089
chattr -i /etc/resolv.conf 2>/dev/null || true
1090+
rm -f /etc/resolv.conf 2>/dev/null || true
10731091
cat > /etc/resolv.conf <<'RESOLVEOF'
1074-
# Temporary fallback written by dnstm-setup (systemd-resolved was stopped)
10751092
nameserver 8.8.8.8
10761093
nameserver 1.1.1.1
10771094
RESOLVEOF
1095+
# Verify the write succeeded
1096+
if ! grep -q '8\.8\.8\.8' /etc/resolv.conf 2>/dev/null; then
1097+
# Last resort: try writing via echo
1098+
echo "nameserver 8.8.8.8" > /etc/resolv.conf 2>/dev/null || true
1099+
fi
10781100
fi
10791101
}
10801102

@@ -1110,6 +1132,26 @@ EOF
11101132
ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf || true
11111133
fi
11121134

1135+
# After relinking, resolv.conf may still reference 127.0.0.53 (dead stub).
1136+
# Also verify DNS actually works — if not, write direct public nameservers.
1137+
sleep 1
1138+
local dns_ok=false
1139+
if getent hosts github.com &>/dev/null 2>&1; then
1140+
dns_ok=true
1141+
elif curl -sf --max-time 3 https://api.ipify.org &>/dev/null 2>&1; then
1142+
dns_ok=true
1143+
fi
1144+
1145+
if [[ "$dns_ok" != "true" ]] || grep -q '127\.0\.0\.53' /etc/resolv.conf 2>/dev/null; then
1146+
print_warn "DNS broken after disabling stub listener — writing fallback nameservers"
1147+
chattr -i /etc/resolv.conf 2>/dev/null || true
1148+
rm -f /etc/resolv.conf 2>/dev/null || true
1149+
cat > /etc/resolv.conf <<'DNSEOF'
1150+
nameserver 8.8.8.8
1151+
nameserver 1.1.1.1
1152+
DNSEOF
1153+
fi
1154+
11131155
return 0
11141156
}
11151157

@@ -1620,7 +1662,7 @@ do_add_tunnel() {
16201662
echo ""
16211663

16221664
# Detect server IP
1623-
SERVER_IP=$(curl -4 -s --max-time 10 https://api.ipify.org 2>/dev/null || true)
1665+
SERVER_IP=$(curl -4 -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 https://icanhazip.com 2>/dev/null || true)
16241666
if [[ -n "$SERVER_IP" ]]; then
16251667
print_ok "Server IP: ${SERVER_IP}"
16261668
fi
@@ -1950,9 +1992,20 @@ do_uninstall() {
19501992
systemctl unmask systemd-resolved.socket systemd-resolved.service 2>/dev/null || true
19511993
systemctl enable systemd-resolved.service 2>/dev/null || true
19521994
systemctl restart systemd-resolved.service 2>/dev/null || true
1995+
sleep 1
19531996
if [[ -e /run/systemd/resolve/stub-resolv.conf ]]; then
19541997
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf || true
19551998
fi
1999+
# Ensure DNS works after uninstall — if resolv.conf is broken, write fallback
2000+
if ! getent hosts google.com >/dev/null 2>&1 && \
2001+
! curl -sf --max-time 3 https://api.ipify.org >/dev/null 2>&1; then
2002+
print_warn "DNS not working after restore — writing fallback nameservers"
2003+
chattr -i /etc/resolv.conf 2>/dev/null || true
2004+
cat > /etc/resolv.conf <<'DNSEOF'
2005+
nameserver 8.8.8.8
2006+
nameserver 1.1.1.1
2007+
DNSEOF
2008+
fi
19562009
print_ok "Restored systemd-resolved defaults (best effort)"
19572010

19582011
echo ""
@@ -2019,11 +2072,23 @@ do_manage_users() {
20192072
# Run initial configure
20202073
print_info "Applying SSH security configuration..."
20212074
mkdir -p /run/sshd 2>/dev/null || true
2075+
# Back up sshd_config before modification
2076+
if [[ -f /etc/ssh/sshd_config ]]; then
2077+
cp -f /etc/ssh/sshd_config /etc/ssh/sshd_config.dnstm-backup 2>/dev/null || true
2078+
fi
20222079
if timeout 30 sshtun-user configure </dev/null 2>&1; then
20232080
print_ok "SSH configuration applied"
20242081
else
20252082
print_warn "SSH configuration may not have applied fully — user management may have issues"
20262083
fi
2084+
# Validate sshd_config — rollback if broken
2085+
if command -v sshd &>/dev/null && ! sshd -t 2>/dev/null; then
2086+
print_warn "sshd_config validation failed — rolling back"
2087+
if [[ -f /etc/ssh/sshd_config.dnstm-backup ]]; then
2088+
cp -f /etc/ssh/sshd_config.dnstm-backup /etc/ssh/sshd_config
2089+
print_ok "Restored sshd_config from backup"
2090+
fi
2091+
fi
20272092
echo ""
20282093
fi
20292094

@@ -3142,7 +3207,7 @@ do_add_xray() {
31423207
fi
31433208

31443209
# Detect server IP
3145-
SERVER_IP=$(curl -4 -s --max-time 10 https://api.ipify.org 2>/dev/null || true)
3210+
SERVER_IP=$(curl -4 -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 https://icanhazip.com 2>/dev/null || true)
31463211
if [[ -n "$SERVER_IP" ]]; then
31473212
print_ok "Server IP: ${SERVER_IP}"
31483213
else
@@ -3583,6 +3648,11 @@ step_preflight() {
35833648
exit 1
35843649
fi
35853650

3651+
# Back up resolv.conf so we can always recover DNS
3652+
if [[ -f /etc/resolv.conf ]] && [[ ! -f /etc/resolv.conf.dnstm-backup ]]; then
3653+
cp -f /etc/resolv.conf /etc/resolv.conf.dnstm-backup 2>/dev/null || true
3654+
fi
3655+
35863656
# Check OS (read in subshell to avoid overwriting script's VERSION variable)
35873657
if [[ -f /etc/os-release ]]; then
35883658
local os_id os_name
@@ -3617,8 +3687,21 @@ step_preflight() {
36173687
fi
36183688
fi
36193689

3690+
# Ensure DNS resolution works (may be broken after previous uninstall)
3691+
if ! curl -4 -s --max-time 3 https://api.ipify.org >/dev/null 2>&1; then
3692+
if grep -q '127\.0\.0\.53' /etc/resolv.conf 2>/dev/null; then
3693+
# systemd-resolved stub is dead — replace with public DNS
3694+
print_warn "DNS broken (stub listener dead) — fixing resolv.conf"
3695+
chattr -i /etc/resolv.conf 2>/dev/null || true
3696+
cat > /etc/resolv.conf <<'DNSEOF'
3697+
nameserver 8.8.8.8
3698+
nameserver 1.1.1.1
3699+
DNSEOF
3700+
fi
3701+
fi
3702+
36203703
# Detect server IP
3621-
SERVER_IP=$(curl -4 -s --max-time 10 https://api.ipify.org 2>/dev/null || true)
3704+
SERVER_IP=$(curl -4 -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 https://icanhazip.com 2>/dev/null || true)
36223705
if [[ -n "$SERVER_IP" ]]; then
36233706
print_ok "Server IP: ${SERVER_IP}"
36243707
else
@@ -3670,6 +3753,17 @@ cloudflare_create_dns_records() {
36703753
local server_ip="$3"
36713754
local cf_api="https://api.cloudflare.com/client/v4"
36723755

3756+
# Ensure jq is installed (needed for JSON parsing)
3757+
if ! command -v jq &>/dev/null; then
3758+
print_info "Installing jq (needed for Cloudflare API)..."
3759+
apt-get update -qq >/dev/null 2>&1 || true
3760+
apt-get install -y -qq jq >/dev/null 2>&1 || true
3761+
if ! command -v jq &>/dev/null; then
3762+
print_fail "Could not install jq. Install manually: apt-get install jq"
3763+
return 1
3764+
fi
3765+
fi
3766+
36733767
# Step 1: Get Zone ID
36743768
print_info "Looking up Cloudflare Zone ID for ${domain}..."
36753769
local zone_resp
@@ -3804,6 +3898,17 @@ step_dns_records() {
38043898
exit 1
38053899
fi
38063900

3901+
# Ensure jq is installed (needed for API JSON parsing)
3902+
if ! command -v jq &>/dev/null; then
3903+
print_info "Installing jq (needed for Cloudflare API)..."
3904+
apt-get update -qq >/dev/null 2>&1 || true
3905+
apt-get install -y -qq jq >/dev/null 2>&1 || true
3906+
if ! command -v jq &>/dev/null; then
3907+
print_fail "Could not install jq. Install manually: apt-get install jq"
3908+
exit 1
3909+
fi
3910+
fi
3911+
38073912
# Validate token
38083913
print_info "Validating API token..."
38093914
local verify_resp
@@ -3902,9 +4007,10 @@ step_free_port53() {
39024007

39034008
# Fallback if stub is still present.
39044009
if echo "$port53_output" | grep -q "systemd-resolve\|127\.0\.0\.53"; then
3905-
print_warn "systemd-resolved still occupies port 53; stopping service as fallback"
4010+
print_warn "systemd-resolved still occupies port 53; stopping + disabling as fallback"
39064011
systemctl stop systemd-resolved.socket 2>/dev/null || true
39074012
systemctl stop systemd-resolved.service 2>/dev/null || true
4013+
systemctl disable systemd-resolved.service 2>/dev/null || true
39084014
ensure_resolv_conf_fallback
39094015
sleep 1
39104016
fi
@@ -4069,9 +4175,10 @@ step_verify_port53() {
40694175
sleep 2
40704176
port53_output=$(ss -ulnp 2>/dev/null | grep -E ':53\b' || true)
40714177
if echo "$port53_output" | grep -q "systemd-resolve\|127\.0\.0\.53"; then
4072-
print_warn "systemd-resolved still occupies :53; stopping service as fallback"
4178+
print_warn "systemd-resolved still occupies :53; stopping + disabling as fallback"
40734179
systemctl stop systemd-resolved.socket 2>/dev/null || true
40744180
systemctl stop systemd-resolved.service 2>/dev/null || true
4181+
systemctl disable systemd-resolved.service 2>/dev/null || true
40754182
ensure_resolv_conf_fallback
40764183
fi
40774184
sleep 2
@@ -4599,6 +4706,12 @@ step_ssh_user() {
45994706
# Configure SSH (only needed once)
46004707
print_info "Applying SSH security configuration..."
46014708
mkdir -p /run/sshd 2>/dev/null || true
4709+
4710+
# Back up sshd_config before any modification
4711+
if [[ -f /etc/ssh/sshd_config ]]; then
4712+
cp -f /etc/ssh/sshd_config /etc/ssh/sshd_config.dnstm-backup 2>/dev/null || true
4713+
fi
4714+
46024715
local configure_output
46034716
configure_output=$(timeout 30 sshtun-user configure </dev/null 2>&1) || true
46044717
if echo "$configure_output" | grep -qi "already"; then
@@ -4610,6 +4723,15 @@ step_ssh_user() {
46104723
print_ok "SSH configuration applied"
46114724
fi
46124725

4726+
# Validate sshd_config — rollback if broken
4727+
if command -v sshd &>/dev/null && ! sshd -t 2>/dev/null; then
4728+
print_warn "sshd_config validation failed — rolling back"
4729+
if [[ -f /etc/ssh/sshd_config.dnstm-backup ]]; then
4730+
cp -f /etc/ssh/sshd_config.dnstm-backup /etc/ssh/sshd_config
4731+
print_ok "Restored sshd_config from backup"
4732+
fi
4733+
fi
4734+
46134735
echo ""
46144736

46154737
# Get username
@@ -5051,7 +5173,7 @@ do_add_domain() {
50515173
fi
50525174

50535175
# Detect server IP
5054-
SERVER_IP=$(curl -4 -s --max-time 10 https://api.ipify.org 2>/dev/null || true)
5176+
SERVER_IP=$(curl -4 -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -4 -s --max-time 5 https://ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 https://icanhazip.com 2>/dev/null || true)
50555177
if [[ -z "$SERVER_IP" ]]; then
50565178
SERVER_IP=$(prompt_input "Enter your server's public IP")
50575179
if [[ -z "$SERVER_IP" ]]; then

0 commit comments

Comments
 (0)