-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathquick-test.sh
More file actions
executable file
·472 lines (383 loc) · 15.6 KB
/
quick-test.sh
File metadata and controls
executable file
·472 lines (383 loc) · 15.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
#!/bin/bash
# Quick Tailscale Exit Node Test
# Fast comparison between exit nodes (~2-3 min)
# Configuration loaded from config.json
set -euo pipefail
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/config.json}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# Store original accept-routes setting
ORIGINAL_ACCEPT_ROUTES=""
# Results storage (associative arrays)
declare -A RESULTS_PING
declare -A RESULTS_TTFB_CLAUDE
declare -A RESULTS_TTFB_OPENAI
declare -A RESULTS_TTFB_GEMINI
declare -A RESULTS_SPEED
declare -a TESTED_NODES=()
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
# ============================================================================
# Tailscale Route Management
# ============================================================================
save_and_disable_accept_routes() {
log_info "Checking current accept-routes setting..."
# Get current prefs from tailscale
local prefs
prefs=$(tailscale debug prefs 2>/dev/null || echo "{}")
# Extract RouteAll (accept-routes) setting
ORIGINAL_ACCEPT_ROUTES=$(echo "$prefs" | grep -o '"RouteAll":[^,}]*' | cut -d: -f2 | tr -d ' ' || echo "false")
if [[ -z "$ORIGINAL_ACCEPT_ROUTES" ]]; then
ORIGINAL_ACCEPT_ROUTES="false"
fi
echo -e " Original accept-routes: ${CYAN}$ORIGINAL_ACCEPT_ROUTES${NC}"
if [[ "$ORIGINAL_ACCEPT_ROUTES" == "true" ]]; then
log_info "Disabling accept-routes to avoid app connector interference..."
sudo tailscale set --accept-routes=false 2>/dev/null || true
sleep 1
log_ok "accept-routes disabled for benchmark"
else
log_ok "accept-routes already disabled, no change needed"
fi
}
restore_accept_routes() {
if [[ "$ORIGINAL_ACCEPT_ROUTES" == "true" ]]; then
log_info "Restoring accept-routes to: $ORIGINAL_ACCEPT_ROUTES"
sudo tailscale set --accept-routes=true 2>/dev/null || true
log_ok "accept-routes restored"
fi
}
# Cleanup function for trap
cleanup() {
local exit_code=$?
echo ""
# Reset exit node
sudo tailscale set --exit-node= 2>/dev/null || true
# Restore accept-routes
restore_accept_routes
if [[ $exit_code -ne 0 ]]; then
log_warn "Script exited with code: $exit_code"
fi
exit $exit_code
}
print_header() {
echo -e "\n${BOLD}${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}${CYAN}║ Quick Exit Node Test - $(date '+%H:%M:%S') ║${NC}"
echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════╝${NC}\n"
}
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
echo -e "${RED}Error: Config file not found: $CONFIG_FILE${NC}"
exit 1
fi
if ! jq empty "$CONFIG_FILE" 2>/dev/null; then
echo -e "${RED}Error: Invalid JSON in config file${NC}"
exit 1
fi
}
set_exit_node() {
local node="$1"
if [[ -z "$node" ]]; then
sudo tailscale set --exit-node= 2>/dev/null || true
else
sudo tailscale set --exit-node="$node" 2>/dev/null || true
fi
sleep 3
# Verify and print current exit node status
local current_exit
current_exit=$(tailscale status --json 2>/dev/null | jq -r '
.Peer | to_entries[] | select(.value.ExitNode == true) | .value.HostName
' 2>/dev/null) || current_exit=""
if [[ -z "$current_exit" ]]; then
echo -e " Exit node: ${GREEN}None (direct connection)${NC}"
else
echo -e " Exit node: ${CYAN}$current_exit${NC}"
fi
}
check_exit_node_available() {
local node_ip="$1"
local node_name="$2"
# Check if the node offers exit node capability
local offers_exit
offers_exit=$(tailscale status 2>/dev/null | grep -E "^${node_ip}\s" | grep -c "offers exit node") || offers_exit=0
if [[ "$offers_exit" -eq 0 ]]; then
log_warn "Node '$node_name' ($node_ip) does not offer exit node capability - SKIPPING"
echo -e " ${YELLOW}Hint: Run 'sudo tailscale up --advertise-exit-node' on that machine${NC}"
return 1
fi
return 0
}
get_ping_avg() {
local host="$1"
local result
result=$(ping -c 5 "$host" 2>/dev/null) || true
echo "$result" | grep -E "rtt|round-trip" | sed 's/.*= //' | cut -d'/' -f2 || echo "N/A"
}
test_node() {
local name="$1"
local ip="$2"
local dns="$3"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}${BOLD}Testing: $name${NC} ($ip)"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
# Track this node
TESTED_NODES+=("$name")
set_exit_node "$ip"
# Public IP check
local public_ip loc
public_ip=$(curl -s --connect-timeout 5 https://ipinfo.io/ip 2>/dev/null || echo "timeout")
loc=$(curl -s --connect-timeout 5 "https://ipinfo.io/$public_ip/city" 2>/dev/null || echo "?")
echo -e "Public IP: ${CYAN}$public_ip${NC} ($loc)"
# Ping
echo -e "\n${BOLD}Ping to 8.8.8.8:${NC}"
local ping_avg
ping_avg=$(get_ping_avg "8.8.8.8")
echo -e " Avg: ${CYAN}${ping_avg}ms${NC}"
RESULTS_PING["$name"]="$ping_avg"
# AI API TTFB from config
echo -e "\n${BOLD}AI API TTFB:${NC}"
local ai_endpoints
readarray -t ai_endpoints < <(jq -r '.ai_endpoints[]' "$CONFIG_FILE" | head -3)
local idx=0
for endpoint in "${ai_endpoints[@]}"; do
local api="${endpoint#https://}"
local ttfb
ttfb=$(curl -w "%{time_starttransfer}" -o /dev/null -s --connect-timeout 10 "$endpoint" 2>/dev/null || echo "N/A")
printf " %-40s TTFB: ${CYAN}%ss${NC}\n" "$api" "$ttfb"
# Store results based on endpoint
case "$api" in
*anthropic*) RESULTS_TTFB_CLAUDE["$name"]="$ttfb" ;;
*openai*) RESULTS_TTFB_OPENAI["$name"]="$ttfb" ;;
*googleapis*|*google*|*gemini*) RESULTS_TTFB_GEMINI["$name"]="$ttfb" ;;
esac
((idx++)) || true
done
# Download
echo -e "\n${BOLD}Download (10MB):${NC}"
local speed speed_mbps
speed=$(curl -w "%{speed_download}" -o /dev/null -s --connect-timeout 30 "https://speed.cloudflare.com/__down?bytes=10485760" 2>/dev/null || echo "0")
if command -v bc &>/dev/null && [[ "$speed" != "0" ]]; then
speed_mbps=$(echo "scale=2; $speed * 8 / 1024 / 1024" | bc 2>/dev/null || echo "0")
else
speed_mbps="N/A"
fi
echo -e " Speed: ${CYAN}${speed_mbps} Mbps${NC}"
RESULTS_SPEED["$name"]="$speed_mbps"
echo ""
}
test_direct() {
local name="Direct"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}${BOLD}Testing: Direct (No Exit Node)${NC}"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
set_exit_node ""
local public_ip loc
public_ip=$(curl -s --connect-timeout 5 https://ipinfo.io/ip 2>/dev/null || echo "timeout")
loc=$(curl -s --connect-timeout 5 "https://ipinfo.io/$public_ip/city" 2>/dev/null || echo "?")
echo -e "Public IP: ${CYAN}$public_ip${NC} ($loc)"
# Ping test
echo -e "\n${BOLD}Ping to 8.8.8.8:${NC}"
local ping_avg
ping_avg=$(get_ping_avg "8.8.8.8")
echo -e " Avg: ${CYAN}${ping_avg}ms${NC}"
RESULTS_PING["$name"]="$ping_avg"
# AI API TTFB
echo -e "\n${BOLD}AI API TTFB (baseline):${NC}"
local ai_endpoints
readarray -t ai_endpoints < <(jq -r '.ai_endpoints[]' "$CONFIG_FILE" | head -3)
for endpoint in "${ai_endpoints[@]}"; do
local api="${endpoint#https://}"
local ttfb
ttfb=$(curl -w "%{time_starttransfer}" -o /dev/null -s --connect-timeout 10 "$endpoint" 2>/dev/null || echo "N/A")
printf " %-40s TTFB: ${CYAN}%ss${NC}\n" "$api" "$ttfb"
# Store results based on endpoint
case "$api" in
*anthropic*) RESULTS_TTFB_CLAUDE["$name"]="$ttfb" ;;
*openai*) RESULTS_TTFB_OPENAI["$name"]="$ttfb" ;;
*googleapis*|*google*|*gemini*) RESULTS_TTFB_GEMINI["$name"]="$ttfb" ;;
esac
done
# Download speed test
echo -e "\n${BOLD}Download (10MB):${NC}"
local speed speed_mbps
speed=$(curl -w "%{speed_download}" -o /dev/null -s --connect-timeout 30 "https://speed.cloudflare.com/__down?bytes=10485760" 2>/dev/null || echo "0")
if command -v bc &>/dev/null && [[ "$speed" != "0" ]]; then
speed_mbps=$(echo "scale=2; $speed * 8 / 1024 / 1024" | bc 2>/dev/null || echo "0")
else
speed_mbps="N/A"
fi
echo -e " Speed: ${CYAN}${speed_mbps} Mbps${NC}"
RESULTS_SPEED["$name"]="$speed_mbps"
echo ""
}
print_summary() {
echo ""
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BOLD}${CYAN}Summary${NC}"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
# Build list of all columns: Direct first, then tested exit nodes
local all_nodes=("Direct" "${TESTED_NODES[@]}")
if [[ ${#TESTED_NODES[@]} -eq 0 ]]; then
echo "No exit nodes tested."
return
fi
local col_width=14
local metric_width=16
# Header
local header=""
header+=$(printf "%-${metric_width}s" "Metric")
for name in "${all_nodes[@]}"; do
if [[ "$name" == "Direct" ]]; then
header+=$(printf " | ${CYAN}%-${col_width}s${NC}" "$name")
else
header+=$(printf " | %-${col_width}s" "$name")
fi
done
header+=$(printf " | %-${col_width}s" "Winner")
echo -e "${BOLD}$header${NC}"
# Separator
local sep=""
sep+=$(printf '%*s' "$metric_width" '' | tr ' ' '-')
for name in "${all_nodes[@]}"; do
sep+="-+-"
sep+=$(printf '%*s' "$col_width" '' | tr ' ' '-')
done
sep+="-+-"
sep+=$(printf '%*s' "$col_width" '' | tr ' ' '-')
echo "$sep"
# Print a metric row
print_metric_row() {
local metric="$1"
local array_name="$2"
local unit="$3"
local lower_better="$4"
local row=""
row+=$(printf "%-${metric_width}s" "$metric")
local best_val=""
local best_name=""
# First pass: find winner (only from TESTED_NODES, not Direct)
for node in "${TESTED_NODES[@]}"; do
local key="${array_name}[$node]"
local val="${!key:-N/A}"
if [[ "$val" != "N/A" && -n "$val" ]]; then
if [[ -z "$best_val" ]]; then
best_val="$val"
best_name="$node"
elif [[ "$lower_better" == "true" ]]; then
if (( $(echo "$val < $best_val" | bc -l 2>/dev/null || echo 0) )); then
best_val="$val"
best_name="$node"
fi
else
if (( $(echo "$val > $best_val" | bc -l 2>/dev/null || echo 0) )); then
best_val="$val"
best_name="$node"
fi
fi
fi
done
# Second pass: build row (include Direct and all tested nodes)
for node in "${all_nodes[@]}"; do
local key="${array_name}[$node]"
local val="${!key:-N/A}"
if [[ "$val" != "N/A" && -n "$val" ]]; then
if [[ "$node" == "Direct" ]]; then
row+=$(printf " | ${CYAN}%-${col_width}s${NC}" "${val}${unit}")
else
row+=$(printf " | %-${col_width}s" "${val}${unit}")
fi
else
row+=$(printf " | %-${col_width}s" "N/A")
fi
done
# Winner column (only from exit nodes, not Direct)
if [[ -n "$best_name" ]]; then
row+=$(printf " | ${GREEN}%-${col_width}s${NC}" "$best_name")
else
row+=$(printf " | %-${col_width}s" "-")
fi
echo -e "$row"
}
# Print metrics
print_metric_row "Ping (avg)" "RESULTS_PING" "ms" "true"
print_metric_row "Claude TTFB" "RESULTS_TTFB_CLAUDE" "s" "true"
print_metric_row "OpenAI TTFB" "RESULTS_TTFB_OPENAI" "s" "true"
print_metric_row "Gemini TTFB" "RESULTS_TTFB_GEMINI" "s" "true"
print_metric_row "Download" "RESULTS_SPEED" "Mbps" "false"
echo ""
echo -e "${CYAN}Note: 'Direct' shown for reference only, not included in winner calculation${NC}"
echo ""
}
main() {
print_header
# Check dependencies
if ! command -v tailscale &>/dev/null; then
echo -e "${RED}Error: tailscale not installed${NC}"
exit 1
fi
if ! command -v jq &>/dev/null; then
echo -e "${RED}Error: jq not installed${NC}"
exit 1
fi
if ! tailscale status &>/dev/null; then
echo -e "${RED}Error: tailscale not running${NC}"
exit 1
fi
load_config
# Set up cleanup trap (handles Ctrl+C, errors, and normal exit)
trap cleanup EXIT INT TERM
# Inform user about sudo usage
echo ""
log_info "This script requires sudo to control Tailscale exit node settings."
log_info "You may be prompted for your password."
echo ""
# Disable accept-routes to avoid app connector interference
save_and_disable_accept_routes
# Clear any existing exit node before starting tests
log_info "Clearing any existing exit node..."
sudo tailscale set --exit-node= 2>/dev/null || true
sleep 3
# Verify exit node is cleared
local current_exit
current_exit=$(tailscale status --json 2>/dev/null | jq -r '
.Peer | to_entries[] | select(.value.ExitNode == true) | .value.HostName
' 2>/dev/null) || current_exit=""
if [[ -z "$current_exit" ]]; then
log_ok "Exit node cleared (direct connection confirmed)"
else
log_warn "Exit node still active: $current_exit"
fi
echo ""
# Test direct connection first
test_direct
# Get exit nodes from config and test each
local node_count
node_count=$(jq '.exit_nodes | length' "$CONFIG_FILE")
for i in $(seq 0 $((node_count - 1))); do
local node_ip node_name node_dns
node_ip=$(jq -r ".exit_nodes[$i].ip" "$CONFIG_FILE")
node_name=$(jq -r ".exit_nodes[$i].name" "$CONFIG_FILE")
node_dns=$(jq -r ".exit_nodes[$i].dns" "$CONFIG_FILE")
echo ""
# Check if node offers exit node capability before testing
if ! check_exit_node_available "$node_ip" "$node_name"; then
echo ""
continue
fi
test_node "$node_name" "$node_ip" "$node_dns"
done
# Print summary table
print_summary
echo -e "${GREEN}${BOLD}Done!${NC}"
# Cleanup (exit node reset + accept-routes restore) handled by trap
}
main "$@"