-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathawg2
More file actions
592 lines (542 loc) · 27.6 KB
/
Copy pathawg2
File metadata and controls
592 lines (542 loc) · 27.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
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
#
# awg2 - hardened, opinionated distribution of bivlked/amneziawg-installer.
#
# Goal: ONE command on a fresh VPS, no flags, and the server stands up a
# road-warrior AmneziaWG 2.0 endpoint already tuned for SERIOUS DPI
# (RU TSPU / Iran / China):
#
# * full-tunnel (route-all) - nothing leaks around the tunnel
# * UDP/443 - blends with QUIC / HTTP-3
# * QUIC-Initial mimicry on I1 - a REAL, per-deploy-unique QUIC v1
# Initial carrying your SNI (lib/quic_i1.py),
# generated offline (no browser tool, no
# shared "7-zip.org" signature)
# * per-deploy randomized Jc/Jmin/Jmax/S1-S4/H1-H4 (done by upstream)
#
# This is an OVERLAY, not a hard fork: upstream bivlked/amneziawg-installer is
# pinned by version and driven through its documented flags + a post-install
# I1 swap. That way upstream's daily churn never rots your patches - bump one
# variable (UPSTREAM_VERSION) to update.
#
# Upstream: https://github.com/bivlked/amneziawg-installer (MIT)
# This tool: https://github.com/antidetect/root.vpn (MIT)
#
# Usage:
# sudo ./awg2 # hardened install (reads ./defaults.conf)
# sudo awg2 add <name> [--expires=7d] [--psk]
# sudo awg2 remove <name>
# sudo awg2 list [-v]
# sudo awg2 rotate-sni <domain> # new QUIC SNI, re-apply, regen all clients
# sudo awg2 rotate-i1 # fresh QUIC Initial (same SNI), regen
# sudo awg2 rotate-reality # new REALITY keypair (TCP/443 leg)
# sudo awg2 rotate-reality-target <host> # change the REALITY decoy site
# sudo awg2 status
# sudo awg2 uninstall
# sudo awg2 help
set -Eeuo pipefail # -E: ERR trap is inherited by functions (so aborts report their line)
# ---------------------------------------------------------------------------
# Locate ourselves / load baked defaults
# ---------------------------------------------------------------------------
SELF_SRC="${BASH_SOURCE[0]:-$0}" # :-$0 so a piped (curl|bash) invocation under set -u doesn't abort
SCRIPT_DIR="$(cd "$(dirname "$SELF_SRC")" >/dev/null 2>&1 && pwd || echo .)"
# -------- baked defaults (overridable by defaults.conf, env, or flags) ------
UPSTREAM_REPO="${UPSTREAM_REPO:-bivlked/amneziawg-installer}"
UPSTREAM_VERSION="${UPSTREAM_VERSION:-v5.18.1}"
UPSTREAM_SCRIPT="${UPSTREAM_SCRIPT:-install_amneziawg_en.sh}"
UPSTREAM_SHA256="${UPSTREAM_SHA256:-}" # optional: pin a known good hash
AWG_SNI="${AWG_SNI:-}" # QUIC SNI to embed (empty -> shape-only)
AWG_PORT="${AWG_PORT:-443}"
AWG_TUNNEL="${AWG_TUNNEL:-full}" # full | amnezia
AWG_PRESET="${AWG_PRESET:-}" # "" | mobile
AWG_SUBNET="${AWG_SUBNET:-}" # "" -> upstream default (10.9.9.0/24)
AWG_MIMICRY="${AWG_MIMICRY:-quic}" # quic | shape | off
AWG_FIRST_CLIENT="${AWG_FIRST_CLIENT:-phone}" # "" to skip
AWG_WORKDIR="${AWG_WORKDIR:-/root/awg-hardened}"
AWG_CONF="${AWG_CONF:-/etc/amnezia/amneziawg/awg0.conf}"
AWG_IFACE="${AWG_IFACE:-awg0}"
SELF_RAW_BASE="${SELF_RAW_BASE:-}" # optional raw URL base to fetch lib/ in curl|bash mode
# -------- TCP/443 leg (Xray VLESS+REALITY) — co-located with AWG/UDP ----------
TCP_ENABLED="${TCP_ENABLED:-0}" # 1 = also stand up the TCP/443 REALITY leg
TCP_PORT="${TCP_PORT:-443}" # TCP (AWG uses UDP/443; no conflict)
TCP_TRANSPORT="${TCP_TRANSPORT:-vision}" # vision (China-leaning) | xhttp (RU-hardened)
REALITY_DEST="${REALITY_DEST:-}" # decoy site host[:443]; "" -> randomized pick
REALITY_SERVERNAME="${REALITY_SERVERNAME:-}" # SNI; defaults to host of REALITY_DEST
XRAY_VERSION="${XRAY_VERSION:-v26.3.27}" # pinned; MUST be >= v25.6.8
XRAY_FP="${XRAY_FP:-chrome}" # client uTLS fingerprint
# MAX extras (optional)
CDN_DOMAIN="${CDN_DOMAIN:-}" # set + CDN_CERT/CDN_KEY to add a CDN-fronted XHTTP+TLS inbound
CDN_CERT="${CDN_CERT:-}"; CDN_KEY="${CDN_KEY:-}"; CDN_PATH="${CDN_PATH:-/}"
CDN_PORT="${CDN_PORT:-8443}"; CDN_PUBLIC_PORT="${CDN_PUBLIC_PORT:-443}"
VLESS_DECRYPTION="${VLESS_DECRYPTION:-none}" # PQ VLESS-Encryption (advanced) server side
VLESS_ENCRYPTION_CLIENT="${VLESS_ENCRYPTION_CLIENT:-none}" # ...and client side
# Load defaults.conf sitting next to the script (preferred way to bake config).
if [ -f "$SCRIPT_DIR/defaults.conf" ]; then
# shellcheck disable=SC1091
. "$SCRIPT_DIR/defaults.conf"
fi
# ---------------------------------------------------------------------------
# UI + i18n + global flags (load helpers; parse --lang/--quiet/--verbose/--ascii)
# ---------------------------------------------------------------------------
QUIET="${QUIET:-0}"; VERBOSE="${VERBOSE:-0}"; AWG_ASCII="${AWG_ASCII:-0}"; RVLANG="${RVLANG:-}"
for _h in ui i18n; do
for _d in "$SCRIPT_DIR/lib" "$AWG_WORKDIR/lib"; do
[ -f "$_d/$_h.sh" ] && { . "$_d/$_h.sh"; break; }
done
done
# Fallback no-op shims if the helper libs (lib/ui.sh, lib/i18n.sh) are missing —
# degrade to plain output, never crash. Vars get safe defaults too (set -u).
: "${UI_TTY:=0}"; : "${B:=}"; : "${DIM:=}"; : "${RED:=}"; : "${GRN:=}"; : "${YEL:=}"; : "${CYN:=}"; : "${R:=}"
: "${G_OK:=*}"; : "${G_WARN:=!}"; : "${G_ERR:=x}"; : "${G_DOT:=-}"
declare -F log >/dev/null || log() { [ "$QUIET" = 1 ] || printf '[awg2] %s\n' "$*"; }
declare -F vlog >/dev/null || vlog() { [ "$VERBOSE" = 1 ] && printf '[awg2] %s\n' "$*"; return 0; }
declare -F ok >/dev/null || ok() { [ "$QUIET" = 1 ] || printf '[ ok ] %s\n' "$*"; }
declare -F warn >/dev/null || warn() { printf '[warn] %s\n' "$*" >&2; }
declare -F die >/dev/null || die() { printf '[fail] %s\n' "$*" >&2; exit 1; }
declare -F t >/dev/null || t() { local k="$1"; shift||true; [ "$#" -eq 0 ] && printf '%s' "$k" || printf "$k" "$@"; }
declare -F section >/dev/null || section() { [ "$QUIET" = 1 ] || printf '\n== %s ==\n' "$*"; }
declare -F rule >/dev/null || rule() { :; }
declare -F step >/dev/null || step() { log "[$1/$2] $3"; }
declare -F card >/dev/null || card() { [ "$QUIET" = 1 ] && return 0; local ti="$1"; shift; printf '\n== %s ==\n' "$ti"; local l; for l in "$@"; do printf ' %s\n' "$l"; done; }
declare -F redact_secrets >/dev/null || redact_secrets() { cat; }
declare -F plainfilter >/dev/null || plainfilter() { cat; }
declare -F ui_init >/dev/null || ui_init() { :; }
declare -F i18n_load >/dev/null || i18n_load() { :; }
# Pull global flags out of the arg list, leave the command + its args in $@.
RV_ARGS=()
parse_global_flags() {
while [ "$#" -gt 0 ]; do
case "$1" in
--lang=*) RVLANG="${1#*=}" ;;
--lang) shift || true; RVLANG="${1:-}" ;;
--quiet|-q) QUIET=1 ;;
--verbose) VERBOSE=1 ;;
--ascii) AWG_ASCII=1 ;;
--no-color) NO_COLOR=1 ;;
--) shift || true; while [ "$#" -gt 0 ]; do RV_ARGS+=("$1"); shift; done; break ;;
*) RV_ARGS+=("$1") ;;
esac
shift || true
done
}
parse_global_flags "$@"
set -- ${RV_ARGS[@]+"${RV_ARGS[@]}"}
export QUIET VERBOSE AWG_ASCII RVLANG NO_COLOR 2>/dev/null || true
i18n_load
ui_init
trap 'die "$(t err.aborted "$LINENO")"' ERR
need_root() { [ "$(id -u)" -eq 0 ] || die "$(t err.need_root)"; }
# A client/profile name we are willing to pass to upstream + use as a path component.
valid_name() {
case "$1" in
''|*[!A-Za-z0-9._-]*) return 1 ;;
.|..|-*) return 1 ;;
*) [ "${#1}" -le 32 ] ;;
esac
}
# AmneziaWG is really installed only if the iface config exists AND carries a key.
awg_installed() {
[ -f "$AWG_CONF" ] && grep -q '^PrivateKey = .' "$AWG_CONF" 2>/dev/null
}
# ---------------------------------------------------------------------------
# Locate upstream's management script (path can vary across upstream versions)
# ---------------------------------------------------------------------------
find_manage() {
local p
for p in /root/awg/manage_amneziawg.sh /etc/amnezia/amneziawg/manage_amneziawg.sh; do
[ -f "$p" ] && { echo "$p"; return 0; }
done
p="$(find /root /etc/amnezia -maxdepth 3 -name manage_amneziawg.sh 2>/dev/null | head -1 || true)"
[ -n "$p" ] && { echo "$p"; return 0; }
return 1
}
manage() {
local m; m="$(find_manage)" || die "manage_amneziawg.sh not found - is AmneziaWG installed? run: sudo awg2"
bash "$m" "$@"
}
# ---------------------------------------------------------------------------
# Resolve lib/quic_i1.py (adjacent, in workdir, or fetched in curl|bash mode)
# ---------------------------------------------------------------------------
locate_lib() {
local cand
for cand in "$SCRIPT_DIR/lib/quic_i1.py" "$AWG_WORKDIR/lib/quic_i1.py"; do
[ -f "$cand" ] && { echo "$cand"; return 0; }
done
if [ -n "$SELF_RAW_BASE" ]; then
mkdir -p "$AWG_WORKDIR/lib"
if curl -fsSL "$SELF_RAW_BASE/lib/quic_i1.py" -o "$AWG_WORKDIR/lib/quic_i1.py" 2>/dev/null; then
echo "$AWG_WORKDIR/lib/quic_i1.py"; return 0
fi
fi
return 1
}
have_quic_crypto() {
command -v python3 >/dev/null 2>&1 || return 1
python3 -c "import cryptography" >/dev/null 2>&1 || return 1
return 0
}
# Emit the I1 value to stdout for the requested mimicry mode.
gen_i1() {
local sni="$1" lib out
case "$AWG_MIMICRY" in
off)
return 1 ;; # keep upstream's I1
quic)
if [ -n "$sni" ] && have_quic_crypto && lib="$(locate_lib)"; then
if out="$(python3 "$lib" --sni "$sni")"; then
printf '%s\n' "$out"; return 0 # real QUIC Initial + SNI
fi
warn "QUIC+SNI generator errored; falling back to shape-only QUIC I1"
else
warn "QUIC+SNI generator unavailable (need SNI + python3-cryptography + lib/quic_i1.py); using shape-only QUIC I1"
fi
;& # graceful fall-through (never aborts -> never leaves a stale I1)
shape)
# Shape-only QUIC long-header CPS template; AmneziaWG fills <r>/<t> fresh
# per packet. No real SNI, but classifiers keying on the QUIC header pass.
local n=$(( 900 + RANDOM % 100 ))
printf '<b 0xc3><b 0x00000001><b 0x08><r 8><b 0x00><b 0x00><r %d><t>\n' "$n"
return 0 ;;
*)
die "unknown AWG_MIMICRY=$AWG_MIMICRY (use quic|shape|off)" ;;
esac
}
# Literal, quoting-safe replace/insert of the I1 line in a config file.
set_i1_in_conf() {
local val="$1" conf="$2"
[ -f "$conf" ] || die "config not found: $conf"
if grep -q '^I1 = ' "$conf"; then
I1NEW="$val" awk 'BEGIN{v=ENVIRON["I1NEW"]} /^I1 = /{print "I1 = " v; next} {print}' \
"$conf" > "$conf.awg2.tmp"
else
# insert right after the [Interface] PrivateKey line
I1NEW="$val" awk 'BEGIN{v=ENVIRON["I1NEW"]; d=0} {print}
/^PrivateKey = /&&!d{print "I1 = " v; d=1}' "$conf" > "$conf.awg2.tmp"
fi
mv "$conf.awg2.tmp" "$conf"
}
apply_quic_i1() {
[ "$AWG_MIMICRY" = "off" ] && { log "AWG_MIMICRY=off - keeping upstream I1"; return 0; }
local i1
# gen_i1 always yields a value for quic/shape; treat anything else as fatal so
# we never leave the server on a new I1 while clients keep the old one.
i1="$(gen_i1 "$AWG_SNI")" || die "I1 generation failed (mimicry=$AWG_MIMICRY)"
[ -n "$i1" ] || die "I1 generator produced empty output (mimicry=$AWG_MIMICRY)"
log "writing QUIC-mimicry I1 to server config (${#i1} chars)"
set_i1_in_conf "$i1" "$AWG_CONF"
log "restarting awg-quick@$AWG_IFACE"
systemctl restart "awg-quick@$AWG_IFACE"
log "regenerating client configs so they mirror the new I1"
# The I1 MUST be byte-identical on server and clients; a swallowed regen
# failure would silently break every tunnel. Surface it and abort.
local rlog; rlog="$(mktemp)"
if ! manage regen >"$rlog" 2>&1; then
# A zero-client regen is not a failure (nothing to mirror yet). Only abort
# when clients actually exist and could be left holding a stale I1.
if find /root/awg -maxdepth 1 -name '*.conf' 2>/dev/null | grep -q .; then
sed -n '1,40p' "$rlog" 2>/dev/null || true
rm -f "$rlog"
die "client regen failed - server I1 changed but clients are stale; fix before distributing configs"
fi
fi
rm -f "$rlog"
}
# ---------------------------------------------------------------------------
# Fetch the pinned upstream installer
# ---------------------------------------------------------------------------
UPSTREAM_INSTALLER="" # set by fetch_upstream (a global, so log/ok/warn stdout is not captured)
fetch_upstream() {
mkdir -p "$AWG_WORKDIR"
local url="https://raw.githubusercontent.com/$UPSTREAM_REPO/$UPSTREAM_VERSION/$UPSTREAM_SCRIPT"
local dst="$AWG_WORKDIR/$UPSTREAM_SCRIPT"
log "fetching upstream $UPSTREAM_REPO@$UPSTREAM_VERSION"
curl -fsSL "$url" -o "$dst" || die "download failed: $url"
if [ -n "$UPSTREAM_SHA256" ]; then
echo "$UPSTREAM_SHA256 $dst" | sha256sum -c - || die "upstream checksum mismatch (refusing to run)"
ok "upstream checksum verified"
else
warn "UPSTREAM_SHA256 not pinned; integrity rests on the immutable git tag $UPSTREAM_VERSION over HTTPS"
log "this build's hash: $(sha256sum "$dst" | cut -d' ' -f1)"
fi
UPSTREAM_INSTALLER="$dst"
}
UPSTREAM_FLAGS=() # populated by build_upstream_flags
build_upstream_flags() {
UPSTREAM_FLAGS=(--yes)
case "$AWG_TUNNEL" in
full) UPSTREAM_FLAGS+=(--route-all) ;;
amnezia) UPSTREAM_FLAGS+=(--route-amnezia) ;;
*) die "unknown AWG_TUNNEL=$AWG_TUNNEL (use full|amnezia)" ;;
esac
UPSTREAM_FLAGS+=("--port=$AWG_PORT")
[ -n "$AWG_PRESET" ] && UPSTREAM_FLAGS+=("--preset=$AWG_PRESET")
[ -n "$AWG_SUBNET" ] && UPSTREAM_FLAGS+=("--subnet=$AWG_SUBNET")
return 0 # never let a trailing false test become the function's exit status
}
validate_config() {
case "$AWG_TUNNEL" in full|amnezia) ;; *) die "AWG_TUNNEL must be full|amnezia (got '$AWG_TUNNEL')";; esac
case "$AWG_MIMICRY" in quic|shape|off) ;; *) die "AWG_MIMICRY must be quic|shape|off (got '$AWG_MIMICRY')";; esac
case "$AWG_PORT" in ''|*[!0-9]*) die "AWG_PORT must be numeric (got '$AWG_PORT')";; esac
[ "$AWG_PORT" -ge 1 ] && [ "$AWG_PORT" -le 65535 ] || die "AWG_PORT out of range: $AWG_PORT"
}
# ---------------------------------------------------------------------------
# Install self into the system so `awg2 <cmd>` works afterwards
# ---------------------------------------------------------------------------
install_self() {
mkdir -p "$AWG_WORKDIR/lib"
cp -f "$SELF_SRC" "$AWG_WORKDIR/awg2" 2>/dev/null || true
[ -f "$SCRIPT_DIR/defaults.conf" ] && cp -f "$SCRIPT_DIR/defaults.conf" "$AWG_WORKDIR/defaults.conf" 2>/dev/null || true
[ -d "$SCRIPT_DIR/lib" ] && cp -rf "$SCRIPT_DIR/lib/." "$AWG_WORKDIR/lib/" 2>/dev/null || true
chmod +x "$AWG_WORKDIR/awg2" 2>/dev/null || true
ln -sf "$AWG_WORKDIR/awg2" /usr/local/sbin/awg2 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Post-install verification (T1..T6 from the build spec)
# ---------------------------------------------------------------------------
self_checks() {
log "running post-install checks"
local fails=0
if awg show "$AWG_IFACE" >/dev/null 2>&1; then ok "interface $AWG_IFACE is up"
else warn "interface $AWG_IFACE not up"; fails=$((fails+1)); fi
if ss -lun 2>/dev/null | grep -q ":$AWG_PORT\b"; then ok "listening on UDP/$AWG_PORT"
else warn "no UDP listener on $AWG_PORT"; fails=$((fails+1)); fi
if [ "$(sysctl -n net.ipv4.ip_forward 2>/dev/null || echo 0)" = "1" ]; then ok "ip_forward enabled"
else warn "ip_forward not enabled"; fails=$((fails+1)); fi
if grep -q '^I1 = ' "$AWG_CONF"; then ok "I1 present in server config"
else warn "no I1 line in $AWG_CONF (mimicry not applied)"; fails=$((fails+1)); fi
# Mirror check: EVERY client's I1 must equal the server's I1.
local srv_i1 cli_i1 f total=0 mism=0
srv_i1="$(grep -m1 '^I1 = ' "$AWG_CONF" || true)"
if [ -n "$srv_i1" ]; then
while IFS= read -r f; do
[ -n "$f" ] || continue
total=$((total + 1))
cli_i1="$(grep -m1 '^I1 = ' "$f" || true)"
[ "$cli_i1" = "$srv_i1" ] || { mism=$((mism + 1)); warn "stale I1 in $(basename "$f")"; }
done < <(find /root/awg -maxdepth 1 -name '*.conf' 2>/dev/null)
if [ "$mism" -gt 0 ]; then warn "$mism/$total client(s) have stale I1 - run 'awg2 rotate-i1'"; fails=$((fails + 1))
elif [ "$total" -gt 0 ]; then ok "all $total client config(s) mirror server I1"; fi
fi
# TCP/443 leg (only if enabled): the Xray service must be active AND listening.
if [ "${TCP_ENABLED:-0}" = "1" ] && declare -f have_xray >/dev/null && have_xray; then
if systemctl is-active --quiet xray 2>/dev/null; then ok "xray service active (TCP leg)"
else warn "xray service not active"; fails=$((fails+1)); fi
if ss -ltn 2>/dev/null | grep -q ":${TCP_PORT:-443} "; then ok "listening on TCP/${TCP_PORT:-443}"
else warn "no TCP listener on ${TCP_PORT:-443}"; fails=$((fails+1)); fi
fi
[ "$fails" -eq 0 ] && ok "all checks passed" || warn "$fails check(s) need attention"
}
print_client() {
local name="${1:-}" # ${1:-} not "$1": avoid set -u abort
[ -n "$name" ] || return 0
local conf="/root/awg/$name.conf"
[ -f "$conf" ] || return 0
card "$(t client.awg_title) ($name)" \
"$(t client.apps_awg)" \
"vpn:// $G_DOT /root/awg/$name.vpnuri ($(t client.qr) /root/awg/$name.vpnuri.png)" \
".conf $G_DOT $conf ($(t client.qr) /root/awg/$name.png)" \
"$YEL$(t client.secret)$R"
# The .conf / vpn:// payloads are large (>~2.5 KB); a terminal QR would exceed
# capacity, so we point to the PNGs upstream already generated. Never abort here.
return 0
}
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
cmd_install() {
need_root
validate_config
section "root.vpn"
log "$(t install.start "$AWG_TUNNEL" "$AWG_PORT" "$AWG_MIMICRY")"
if [ "$AWG_MIMICRY" = "quic" ] && [ -z "$AWG_SNI" ]; then
warn "AWG_SNI is empty -> falling back to shape-only QUIC I1 (no embedded SNI)."
warn "For real SNI mimicry vs serious DPI, set AWG_SNI in defaults.conf and re-run, or 'awg2 rotate-sni <domain>'."
fi
if [ "$AWG_MIMICRY" = "quic" ] && [ -n "$AWG_SNI" ]; then
log "ensuring python3-cryptography for QUIC I1 generation"
if ! have_quic_crypto; then
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y python3 python3-cryptography >/dev/null 2>&1 \
|| warn "could not install python3-cryptography; will fall back to shape-only I1"
fi
fi
if awg_installed; then
log "$(t install.awg_present)"
else
fetch_upstream; local installer="$UPSTREAM_INSTALLER"
build_upstream_flags
vlog "running upstream: bash $UPSTREAM_SCRIPT ${UPSTREAM_FLAGS[*]}"
local irc=0
if ( cd "$AWG_WORKDIR" && AWG_YES=1 bash "$installer" "${UPSTREAM_FLAGS[@]}" ); then irc=0; else irc=$?; fi
if ! awg_installed; then
# The upstream installer reboots after a kernel/setup step (esp. on fresh
# images). That is expected, not a failure — resume on the next run.
echo
warn "$(t reboot.resume)"
warn " sudo awg2"
exit 0
fi
ok "$(t common.done 2>/dev/null || echo "AmneziaWG base install finished")"
fi
apply_quic_i1
install_self
# --- optional TCP/443 leg: Xray VLESS+REALITY, co-located with AWG/UDP ---
local tcp_on=0
if [ "${TCP_ENABLED:-0}" = "1" ]; then
if declare -f xray_setup >/dev/null; then
log "$(t install.tcp_setup "$TCP_TRANSPORT")"; xray_setup; tcp_on=1
else warn "TCP_ENABLED=1 but lib/xray.sh not found — TCP/443 leg skipped"; fi
fi
if [ -n "$AWG_FIRST_CLIENT" ] && valid_name "$AWG_FIRST_CLIENT"; then
if ! manage list 2>/dev/null | grep -Fqw "$AWG_FIRST_CLIENT"; then
log "$(t install.first_client "$AWG_FIRST_CLIENT")"
manage add "$AWG_FIRST_CLIENT" >/dev/null 2>&1 || warn "add $AWG_FIRST_CLIENT failed"
fi
# Regenerate it from the live (patched) awg0.conf in case 'add' used cached
# params; self_checks then verifies the mirror across ALL clients.
if [ "$AWG_MIMICRY" != "off" ]; then
manage regen "$AWG_FIRST_CLIENT" >/dev/null 2>&1 || warn "regen of $AWG_FIRST_CLIENT failed - check 'awg2 status'"
fi
[ "$tcp_on" = "1" ] && xray_add_client "$AWG_FIRST_CLIENT"
fi
self_checks
# ---- final summary card --------------------------------------------------
local svc_awg="awg-quick@$AWG_IFACE" lines=()
lines+=("$(t summary.awg "$AWG_PORT" "$svc_awg")")
if [ "$tcp_on" = "1" ]; then
lines+=("$(t summary.tcp "$TCP_PORT" "xray" "${REALITY_DEST:-auto}")")
else
lines+=("$(t summary.tcp_off)")
fi
lines+=("$(t summary.manage)")
section ""
card "$(t summary.title)" "${lines[@]}"
log "$(t summary.udponly)"
if [ -n "$AWG_FIRST_CLIENT" ] && valid_name "$AWG_FIRST_CLIENT"; then
print_client "$AWG_FIRST_CLIENT"
[ "$tcp_on" = "1" ] && xray_print_client "$AWG_FIRST_CLIENT"
fi
}
cmd_rotate_sni() {
need_root
local domain="${1:-}"; [ -n "$domain" ] || die "usage: awg2 rotate-sni <domain>"
AWG_SNI="$domain"; AWG_MIMICRY="quic"
if ! have_quic_crypto; then
apt-get update -y >/dev/null 2>&1 || true
apt-get install -y python3 python3-cryptography >/dev/null 2>&1 || true
fi
apply_quic_i1
self_checks
ok "SNI rotated to '$domain'. Re-distribute updated client configs from /root/awg/."
}
cmd_rotate_i1() {
need_root
apply_quic_i1
self_checks
ok "I1 regenerated. Re-distribute updated client configs from /root/awg/."
}
cmd_status() {
need_root
section "$(t status.title)"
# Truncate the (long, non-secret) i1 mimicry blob so status stays readable.
{ awg show "$AWG_IFACE" 2>/dev/null || warn "interface not up"; } \
| sed -E 's/^([[:space:]]*i1: <b 0x[0-9a-f]{12})[0-9a-f]+.*/\1…>/'
echo; { grep -E '^(ListenPort|I1|H1|H2|H3|H4|S1|S2|S3|S4|Jc|Jmin|Jmax) ' "$AWG_CONF" 2>/dev/null || true; } | sed 's/^I1 = .*/I1 = <quic-mimicry blob hidden>/'
echo; manage list -v 2>/dev/null | plainfilter || true
if [ "${TCP_ENABLED:-0}" = "1" ] && declare -f xray_status >/dev/null; then echo; log "TCP/443 leg (Xray VLESS+REALITY):"; xray_status; fi
}
# add a client to BOTH legs (AWG/UDP + VLESS/TCP), print both QRs
cmd_add() {
need_root
local name="${1:-}"; [ -n "$name" ] || die "usage: awg2 add <name> [--expires=DUR] [--psk]"
valid_name "$name" || die "$(t err.bad_name "$name")"
if ! manage list 2>/dev/null | grep -Fqw "$name"; then
manage add "$@" >/dev/null 2>&1 || warn "AWG add failed for $name"
fi
[ "$AWG_MIMICRY" != "off" ] && manage regen "$name" >/dev/null 2>&1 || true
print_client "$name"
if [ "${TCP_ENABLED:-0}" = "1" ] && declare -f xray_add_client >/dev/null && have_xray; then
xray_add_client "$name"
xray_print_client "$name"
fi
}
cmd_remove() {
need_root
local name="${1:-}"; [ -n "$name" ] || die "usage: awg2 remove <name>"
valid_name "$name" || die "$(t err.bad_name "$name")"
manage remove "$name" >/dev/null 2>&1 || warn "AWG remove failed for $name"
if [ "${TCP_ENABLED:-0}" = "1" ] && declare -f xray_remove_client >/dev/null; then
xray_remove_client "$name" || warn "TCP remove failed for $name"
fi
ok "removed $name"
}
cmd_list() {
need_root
log "AmneziaWG (UDP/443) clients:"; manage list "$@" 2>/dev/null | plainfilter || true
if [ "${TCP_ENABLED:-0}" = "1" ] && declare -f xray_status >/dev/null && [ -f "${XRAY_CLIENTS:-/nonexistent}" ]; then
echo; log "VLESS+REALITY (TCP/443) clients:"; { cut -f1 "$XRAY_CLIENTS" 2>/dev/null | sed 's/^/ /'; } || true
fi
}
usage() {
awk 'NR>=3 && /^#/ {sub(/^# ?/,""); print; next} NR>=3 {exit}' "$SELF_SRC"
}
# Load the optional TCP/443 (Xray VLESS+REALITY) leg if present.
for _xs in "$SCRIPT_DIR/lib/xray.sh" "$AWG_WORKDIR/lib/xray.sh"; do
[ -f "$_xs" ] && { . "$_xs"; break; }
done
# ---------------------------------------------------------------------------
# Interactive menu — only when run with NO command AND on a real terminal.
# Reads from /dev/tty so it never hijacks a `curl | sudo bash` pipe (whose stdin
# is the script itself); in that non-TTY case we fall straight through to install.
# ---------------------------------------------------------------------------
menu() {
[ "$#" -eq 0 ] || return 1 # only with zero args
[ "$QUIET" = 1 ] && return 1 # scripted/quiet -> just install
[ -t 0 ] || [ -r /dev/tty ] || return 1
[ "${UI_TTY:-0}" = 1 ] || return 1 # not a dumb/redirected terminal
section "$(t menu.title)"
printf ' %s\n %s\n %s\n %s\n %s\n %s\n' \
"$(t menu.install)" "$(t menu.add)" "$(t menu.list)" \
"$(t menu.status)" "$(t menu.rotate)" "$(t menu.uninstall)"
local choice=""; printf '%s' "$(t menu.prompt)" >&2
IFS= read -r choice </dev/tty || choice=1
[ -n "$choice" ] || choice=1
case "$choice" in
1) cmd_install ;;
2) local n=""; printf '%s' "$(t menu.askname)" >&2; IFS= read -r n </dev/tty || true; cmd_add "$n" ;;
3) cmd_list ;;
4) cmd_status ;;
5) if declare -f cmd_rotate_reality >/dev/null; then cmd_rotate_reality; else die "$(t err.tcp_unavailable)"; fi ;;
6) printf '%s' "$(t uninstall.confirm)" >&2; local c=""; IFS= read -r c </dev/tty || true
if [ "$c" = "yes" ]; then exec "$SELF_SRC" --lang="$RVLANG" uninstall; else warn "$(t menu.invalid)"; fi ;;
*) die "$(t menu.invalid)" ;;
esac
return 0
}
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
if [ "$#" -eq 0 ] && menu; then
exit 0
fi
cmd="${1:-install}"
case "$cmd" in
install|"") shift || true; cmd_install "$@" ;;
add) cmd_add "${@:2}" ;;
remove) cmd_remove "${2:-}" ;;
list) cmd_list "${@:2}" ;;
stats|show|regen|backup|restore|check|diagnose|repair-module|restart)
need_root; manage "$cmd" "${@:2}" ;;
rotate-sni) cmd_rotate_sni "${2:-}" ;;
rotate-i1) cmd_rotate_i1 ;;
rotate-reality) if declare -f cmd_rotate_reality >/dev/null; then cmd_rotate_reality; else die "$(t err.tcp_unavailable)"; fi ;;
rotate-reality-target) if declare -f cmd_rotate_reality_target >/dev/null; then cmd_rotate_reality_target "${2:-}"; else die "$(t err.tcp_unavailable)"; fi ;;
status) cmd_status ;;
uninstall) need_root; ( cd "$AWG_WORKDIR" 2>/dev/null && AWG_YES=1 bash "$UPSTREAM_SCRIPT" --uninstall --yes ) || manage uninstall || true
declare -f xray_uninstall >/dev/null && xray_uninstall || true
rm -rf /etc/root.vpn 2>/dev/null || true
ok "$(t common.done)" ;;
help|-h|--help) usage ;;
*) die "$(t err.unknown_cmd "$cmd")" ;;
esac