Skip to content

Commit 59a866d

Browse files
authored
feat(scripts): add verify-umbrel.sh diagnostic script for Umbrel users (#201)
Adds verify-umbrel.sh to tunnelsats/scripts to allow Umbrel Official Store users (running in SECURE_MODE=true) to download and run connection diagnostics via wget/curl out-of-band.
1 parent 199a781 commit 59a866d

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

scripts/verify-umbrel.sh

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/bin/bash
2+
# TunnelSats Unified Verification & Diagnostics for Umbrel
3+
# Consolidates installation proofing and dataplane connectivity tests.
4+
5+
set -euo pipefail
6+
7+
# Colors
8+
GREEN='\033[0;32m'
9+
RED='\033[0;31m'
10+
YELLOW='\033[1;33m'
11+
BLUE='\033[0;34m'
12+
NC='\033[0m'
13+
14+
LEAN=false
15+
if [[ "$*" == *"--lean"* ]]; then LEAN=true; fi
16+
ALLOW_SKIP=false
17+
if [[ "$*" == *"--allow-skip"* ]]; then ALLOW_SKIP=true; fi
18+
19+
log_info() { if [ "$LEAN" = false ]; then echo -e "${GREEN}[INFO]${NC} $1"; fi; }
20+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
21+
22+
usage() {
23+
echo "Usage: $0 [node|dataplane] [--lean] [--allow-skip]"
24+
exit 1
25+
}
26+
27+
# Config paths for dataplane metadata discovery
28+
META_PATHS=(
29+
"/data/tunnelsats-meta.json"
30+
"${HOME:-/home/umbrel}/umbrel/app-data/tunnelsats-data/tunnelsats-meta.json"
31+
"${HOME:-/home/umbrel}/umbrel/app-data/tunnelsats/data/tunnelsats-meta.json"
32+
"/home/umbrel/umbrel/app-data/tunnelsats-data/tunnelsats-meta.json"
33+
"/home/umbrel/umbrel/app-data/tunnelsats/data/tunnelsats-meta.json"
34+
"/umbrel/app-data/tunnelsats-data/tunnelsats-meta.json"
35+
"/umbrel/app-data/tunnelsats/data/tunnelsats-meta.json"
36+
)
37+
38+
run_node_check() {
39+
log_info "Verifying Umbrel App State..."
40+
if ! command -v docker &> /dev/null; then log_error "Docker not found"; return 1; fi
41+
42+
if ! DOCKER_ERR=$(docker ps 2>&1); then
43+
log_error "Docker access failed: $DOCKER_ERR"
44+
return 1
45+
fi
46+
47+
CONTAINER_ID=$(docker ps -aqf "name=tunnelsats" | head -n 1)
48+
if [ -z "$CONTAINER_ID" ]; then
49+
log_error "TunnelSats container not found."
50+
return 1
51+
fi
52+
53+
STATE=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_ID")
54+
log_info "Container State: $STATE"
55+
if [ "$STATE" != "running" ]; then return 1; fi
56+
}
57+
58+
run_dataplane() {
59+
# 1. Metadata discovery (Global ENVs take precedence)
60+
VPN_IP="${VPN_IP:-}"
61+
VPN_HOST="${VPN_HOST:-}"
62+
VPN_PORT="${VPN_PORT:-}"
63+
64+
if [ -z "$VPN_IP" ] || [ -z "$VPN_PORT" ] || [ -z "$VPN_HOST" ]; then
65+
for p in "${META_PATHS[@]}"; do
66+
if [ -f "$p" ] && command -v jq &> /dev/null; then
67+
RAW_VAL=$(jq -r '(.vpn_ip // .vpnIP // .wgEndpoint // empty)' "$p" 2>/dev/null || true)
68+
# Only overwrite if currently empty
69+
[ -z "$VPN_HOST" ] && VPN_HOST=$(jq -r '(.vpn_host // .serverDomain // empty)' "$p" 2>/dev/null || true)
70+
[ -z "$VPN_PORT" ] && VPN_PORT=$(jq -r '(.vpn_port // .vpnPort // empty)' "$p" 2>/dev/null || true)
71+
72+
if [ -n "$RAW_VAL" ] && [ -z "$VPN_IP" ]; then
73+
DOMAIN="${RAW_VAL%%:*}"
74+
if [[ "$DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
75+
VPN_IP="$DOMAIN"
76+
else
77+
VPN_IP=$(getent hosts "$DOMAIN" | awk '{ print $1 }' | head -n 1 || true)
78+
fi
79+
fi
80+
[ -n "$VPN_IP" ] && [ -n "$VPN_PORT" ] && [ -n "$VPN_HOST" ] && break
81+
fi
82+
done
83+
fi
84+
85+
if [ -z "$VPN_IP" ] || [ -z "$VPN_PORT" ]; then
86+
log_error "No active VPN metadata found."
87+
return 1
88+
fi
89+
90+
# Fallback host display
91+
[ -z "$VPN_HOST" ] && VPN_HOST="$VPN_IP"
92+
93+
if [ "$LEAN" = false ]; then
94+
echo -e "${BLUE}=== TunnelSats Dataplane Verification ===${NC}"
95+
echo -e "${YELLOW}Target: ${NC}${VPN_HOST} (${VPN_IP}) : ${VPN_PORT}"
96+
echo -e "----------------------------------------------------------------"
97+
fi
98+
99+
FAILED_TESTS=0
100+
SKIPPED_TESTS=0
101+
check_result() {
102+
if [ $1 -eq 0 ]; then
103+
echo -e "${GREEN}PASS${NC} ($2)"
104+
else
105+
echo -e "${RED}FAIL${NC} ($2)"
106+
FAILED_TESTS=$((FAILED_TESTS + 1))
107+
fi
108+
}
109+
check_skipped() {
110+
echo -e "${BLUE}SKIPPED${NC} ($1)"
111+
SKIPPED_TESTS=$((SKIPPED_TESTS + 1))
112+
}
113+
114+
# 2. Home IP
115+
echo -ne "${YELLOW}[0/3] Discovering Home IP... ${NC} "
116+
HOME_IP=$(curl -sL --max-time 10 ifconfig.me 2>/dev/null || echo "TIMEOUT")
117+
if [ "$HOME_IP" != "TIMEOUT" ]; then
118+
check_result 0 "${HOME_IP}"
119+
else
120+
check_result 1 "Failed to resolve Home IP"
121+
fi
122+
123+
# 3. Outbound Tunnel Verification
124+
echo -ne "${YELLOW}[1/3] Testing Outbound Tunnel Alignment... ${NC} "
125+
EXEC_ERR=$(docker exec tunnelsats true 2>&1 || true)
126+
if [ -z "$EXEC_ERR" ]; then
127+
OUTBOUND=$(docker exec tunnelsats curl -sL --interface tunnelsatsv2 --max-time 10 ifconfig.me 2>/dev/null || echo "TIMEOUT")
128+
if [[ "$OUTBOUND" == "$VPN_IP" ]]; then
129+
check_result 0 "Verified via $VPN_IP"
130+
else
131+
check_result 1 "Leak/Timeout (Got: $OUTBOUND)"
132+
fi
133+
elif echo "$EXEC_ERR" | grep -qi "permission denied"; then
134+
check_skipped "Requires sudo"
135+
else
136+
check_result 1 "Container not found or not running"
137+
fi
138+
139+
# 4. Inbound IP Test
140+
echo -ne "${YELLOW}[2/3] Testing Inbound Port (via IP)... ${NC} "
141+
if timeout 5s bash -c 'true > /dev/tcp/"$1"/"$2"' _ "$VPN_IP" "$VPN_PORT" 2>/dev/null; then
142+
check_result 0 "Connected to ${VPN_IP}:${VPN_PORT}"
143+
else
144+
check_result 1 "Connection Refused/Timeout"
145+
fi
146+
147+
# 5. Inbound Hostname Test
148+
echo -ne "${YELLOW}[3/3] Testing Inbound Port (via Hostname)... ${NC} "
149+
if timeout 5s bash -c 'true > /dev/tcp/"$1"/"$2"' _ "$VPN_HOST" "$VPN_PORT" 2>/dev/null; then
150+
check_result 0 "Connected to ${VPN_HOST}:${VPN_PORT}"
151+
else
152+
check_result 1 "DNS Failure or Connection Refused"
153+
fi
154+
155+
echo -e "----------------------------------------------------------------"
156+
if [ $FAILED_TESTS -gt 0 ] || { [ "$ALLOW_SKIP" = false ] && [ $SKIPPED_TESTS -gt 0 ]; }; then
157+
return 1
158+
fi
159+
}
160+
161+
COMMAND="dataplane"
162+
for arg in "$@"; do
163+
case "$arg" in
164+
node|dataplane) COMMAND="$arg" ;;
165+
--lean|--allow-skip) ;;
166+
*) usage ;;
167+
esac
168+
done
169+
170+
case "$COMMAND" in
171+
node) run_node_check ;;
172+
dataplane) run_dataplane ;;
173+
*) usage ;;
174+
esac

0 commit comments

Comments
 (0)