Last active 1787200824

Script bash install Cloudflare Warp di vps Ipv6-Only biar bisa koneksi ke Ipv4. Credit: Shirayuki (@ardvzxx)

warp-ipv6.sh Raw
1#!/usr/bin/env bash
2
3set -Eeuo pipefail
4IFS=$'\n\t'
5export LC_ALL=C
6
7readonly SCRIPT_NAME="${0##*/}"
8readonly SCRIPT_VERSION="2.0.1"
9readonly BOOTSTRAP_SERVICE="warp-ipv4-bootstrap.service"
10readonly BOOTSTRAP_UNIT="/etc/systemd/system/${BOOTSTRAP_SERVICE}"
11readonly BOOTSTRAP_HELPER="/usr/local/libexec/warp-ipv4-bootstrap"
12readonly WARP_DROPIN_DIR="/etc/systemd/system/warp-svc.service.d"
13readonly WARP_DROPIN_FILE="${WARP_DROPIN_DIR}/20-ipv4-bootstrap.conf"
14readonly LEGACY_DROPIN_FILE="${WARP_DROPIN_DIR}/override.conf"
15readonly STATE_DIR="/var/lib/warp-ipv4-egress"
16readonly STATE_FILE="${STATE_DIR}/state.env"
17readonly BACKUP_DIR="/root/warp-ipv4-egress-backups"
18readonly ROLLBACK_SCRIPT="/root/warp-ipv4-egress-rollback.sh"
19readonly ROLLBACK_UNIT="warp-ipv4-egress-rollback-${$}"
20readonly LEGACY_ROLLBACK_UNIT="warp-ipv4-egress-rollback"
21readonly LOCK_FILE="/run/lock/warp-ipv4-egress-install.lock"
22
23DUMMY_IPV4="${DUMMY_IPV4:-192.168.254.2}"
24DUMMY_PREFIX="${DUMMY_PREFIX:-32}"
25DUMMY_GATEWAY="${DUMMY_GATEWAY:-192.168.254.1}"
26DUMMY_METRIC="${DUMMY_METRIC:-65000}"
27FRESH_INSTALL="${FRESH_INSTALL:-1}"
28ALLOW_IPV4_SSH="${ALLOW_IPV4_SSH:-0}"
29CHECK_ONLY=0
30
31IFACE="${IFACE:-}"
32VERSION_CODENAME=""
33PRETTY_NAME=""
34ARCH=""
35IP_BIN=""
36
37OLD_IFACE=""
38OLD_DUMMY_IPV4=""
39OLD_DUMMY_PREFIX=""
40OLD_DUMMY_GATEWAY=""
41OLD_DUMMY_METRIC=""
42
43TMP_DIR=""
44BACKUP_FILE=""
45DIAG_FILE=""
46TEMP_TUN=""
47ROLLBACK_ARMED=0
48WARP_MASKED_BY_SCRIPT=0
49WARP_ACCEPT_TOS=0
50INSTALL_SUCCESS=0
51LOCK_FD=9
52
53APT_OPTS=(
54 -o Acquire::ForceIPv6=true
55 -o Acquire::Retries=3
56 -o Dpkg::Use-Pty=0
57)
58
59usage() {
60 cat <<EOF_USAGE
61${SCRIPT_NAME} ${SCRIPT_VERSION}
62
63Cloudflare WARP IPv4 egress untuk VPS Ubuntu IPv6-only.
64
65Opsi:
66 --check-only Jalankan pemeriksaan host tanpa instalasi/pembersihan.
67 --fresh Paksa instalasi fresh (default).
68 --no-fresh Pertahankan registrasi/state WARP yang sudah ada.
69 -h, --help Tampilkan bantuan.
70
71Environment override:
72 IFACE, DUMMY_IPV4, DUMMY_PREFIX, DUMMY_GATEWAY, DUMMY_METRIC,
73 FRESH_INSTALL=0|1, ALLOW_IPV4_SSH=0|1
74EOF_USAGE
75}
76
77log() {
78 printf '\n\033[1;34m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*"
79}
80
81ok() {
82 printf '\033[1;32m[OK]\033[0m %s\n' "$*"
83}
84
85warn() {
86 printf '\033[1;33m[WARN]\033[0m %s\n' "$*" >&2
87}
88
89collect_diagnostics() {
90 local reason="${1:-unknown error}"
91
92 [[ ${EUID:-1} -eq 0 ]] || return 0
93 if ! command -v systemctl >/dev/null 2>&1 \
94 && ! command -v warp-cli >/dev/null 2>&1; then
95 return 0
96 fi
97
98 DIAG_FILE="/root/warp-ipv4-egress-diagnostics-$(date '+%Y%m%d-%H%M%S').log"
99 {
100 printf 'Reason: %s\n' "$reason"
101 printf 'Date: %s\n' "$(date -Is 2>/dev/null || date)"
102 printf 'Script: %s %s\n' "$SCRIPT_NAME" "$SCRIPT_VERSION"
103 printf 'Interface: %s\n' "${IFACE:-unknown}"
104 printf '\n===== OS =====\n'
105 cat /etc/os-release 2>/dev/null || true
106 uname -a 2>/dev/null || true
107 printf '\n===== IPv4 addresses/routes =====\n'
108 ip -4 addr show 2>/dev/null || true
109 ip -4 route show table all 2>/dev/null || true
110 printf '\n===== IPv6 addresses/routes =====\n'
111 ip -6 addr show 2>/dev/null || true
112 ip -6 route show table all 2>/dev/null || true
113 printf '\n===== warp-cli version/status =====\n'
114 warp-cli --version 2>&1 || true
115 timeout 10 warp-cli --accept-tos status 2>&1 \
116 || timeout 10 warp-cli status 2>&1 \
117 || true
118 printf '\n===== warp-svc status =====\n'
119 systemctl status warp-svc.service --no-pager 2>&1 || true
120 printf '\n===== bootstrap status =====\n'
121 systemctl status "$BOOTSTRAP_SERVICE" --no-pager 2>&1 || true
122 printf '\n===== warp-svc journal =====\n'
123 journalctl -u warp-svc.service -n 180 --no-pager 2>&1 || true
124 } >"$DIAG_FILE" 2>&1 || true
125
126 chmod 0600 "$DIAG_FILE" 2>/dev/null || true
127 warn "Diagnostik disimpan di ${DIAG_FILE}"
128}
129
130fatal() {
131 local message="$*"
132 printf '\033[1;31m[ERROR]\033[0m %s\n' "$message" >&2
133 collect_diagnostics "$message" || true
134 exit 1
135}
136
137on_error() {
138 local exit_code=$?
139 local line_no="${BASH_LINENO[0]:-unknown}"
140 local failed_command="${BASH_COMMAND:-unknown}"
141
142 [[ $- == *e* ]] || return 0
143
144 trap - ERR
145 set +e
146
147 printf '\033[1;31m[ERROR]\033[0m Kegagalan tak terduga (exit %s) di baris %s.\n' \
148 "$exit_code" "$line_no" >&2
149 printf ' Perintah: %s\n' "$failed_command" >&2
150 collect_diagnostics "unexpected exit ${exit_code} at line ${line_no}: ${failed_command}" || true
151
152 return 0
153}
154
155on_exit() {
156 local exit_code=$?
157 trap - EXIT INT TERM HUP
158 set +e
159
160 if [[ -n "$TEMP_TUN" ]]; then
161 ip link delete "$TEMP_TUN" >/dev/null 2>&1 || true
162 fi
163
164 if (( exit_code != 0 && ROLLBACK_ARMED == 1 )); then
165 warn "Instalasi tidak selesai. Menjalankan rollback untuk memulihkan jalur IPv6/SSH."
166 systemctl stop "${ROLLBACK_UNIT}.timer" "${ROLLBACK_UNIT}.service" >/dev/null 2>&1 || true
167 bash "$ROLLBACK_SCRIPT" >/dev/null 2>&1 || true
168 fi
169
170 if (( WARP_MASKED_BY_SCRIPT == 1 )); then
171 systemctl unmask --runtime warp-svc.service >/dev/null 2>&1 || true
172 WARP_MASKED_BY_SCRIPT=0
173 fi
174
175 if [[ -n "$TMP_DIR" && -d "$TMP_DIR" ]]; then
176 rm -rf "$TMP_DIR"
177 fi
178
179 exit "$exit_code"
180}
181
182trap on_error ERR
183trap on_exit EXIT
184trap 'exit 130' INT
185trap 'exit 143' TERM HUP
186
187parse_args() {
188 while (($#)); do
189 case "$1" in
190 --check-only)
191 CHECK_ONLY=1
192 ;;
193 --fresh)
194 FRESH_INSTALL=1
195 ;;
196 --no-fresh)
197 FRESH_INSTALL=0
198 ;;
199 -h|--help)
200 usage
201 exit 0
202 ;;
203 *)
204 printf 'Opsi tidak dikenal: %s\n\n' "$1" >&2
205 usage >&2
206 exit 2
207 ;;
208 esac
209 shift
210 done
211}
212
213require_root() {
214 [[ ${EUID} -eq 0 ]] || fatal "Jalankan script ini sebagai root."
215}
216
217validate_boolean() {
218 [[ "$1" == "0" || "$1" == "1" ]]
219}
220
221validate_ipv4() {
222 local value="$1"
223 local IFS=.
224 local -a octets
225 local octet
226
227 read -r -a octets <<<"$value"
228 [[ ${#octets[@]} -eq 4 ]] || return 1
229
230 for octet in "${octets[@]}"; do
231 [[ "$octet" =~ ^[0-9]+$ ]] || return 1
232 ((10#$octet >= 0 && 10#$octet <= 255)) || return 1
233 done
234}
235
236validate_iface() {
237 [[ "$1" =~ ^[a-zA-Z0-9_.:-]+$ ]]
238}
239
240check_inputs() {
241 validate_boolean "$FRESH_INSTALL" || fatal "FRESH_INSTALL harus 0 atau 1."
242 validate_boolean "$ALLOW_IPV4_SSH" || fatal "ALLOW_IPV4_SSH harus 0 atau 1."
243 validate_ipv4 "$DUMMY_IPV4" || fatal "DUMMY_IPV4 tidak valid: $DUMMY_IPV4"
244 validate_ipv4 "$DUMMY_GATEWAY" || fatal "DUMMY_GATEWAY tidak valid: $DUMMY_GATEWAY"
245
246 [[ "$DUMMY_PREFIX" =~ ^[0-9]+$ ]] \
247 && ((DUMMY_PREFIX >= 0 && DUMMY_PREFIX <= 32)) \
248 || fatal "DUMMY_PREFIX harus 0-32."
249
250 [[ "$DUMMY_METRIC" =~ ^[0-9]+$ ]] \
251 && ((DUMMY_METRIC >= 1 && DUMMY_METRIC <= 4294967295)) \
252 || fatal "DUMMY_METRIC tidak valid."
253}
254
255check_platform() {
256 [[ -r /etc/os-release ]] || fatal "/etc/os-release tidak ditemukan."
257 source /etc/os-release
258
259 [[ "${ID:-}" == "ubuntu" ]] \
260 || fatal "Script ini khusus Ubuntu. Terdeteksi: ${PRETTY_NAME:-unknown}"
261
262 case "${VERSION_CODENAME:-}" in
263 jammy|noble|resolute) ;;
264 *)
265 fatal "Ubuntu ${VERSION_CODENAME:-unknown} tidak didukung oleh repository WARP resmi. Target: jammy/noble/resolute."
266 ;;
267 esac
268
269 ARCH="$(dpkg --print-architecture 2>/dev/null || true)"
270 [[ -n "$ARCH" ]] || fatal "Arsitektur dpkg tidak dapat dideteksi."
271
272 ok "OS: ${PRETTY_NAME} (${VERSION_CODENAME}), arsitektur ${ARCH}"
273}
274
275check_systemd() {
276 [[ -d /run/systemd/system ]] \
277 || fatal "systemd tidak aktif sebagai init system. Script ini membutuhkan systemd."
278
279 command -v systemctl >/dev/null 2>&1 || fatal "systemctl tidak ditemukan."
280 command -v systemd-run >/dev/null 2>&1 || fatal "systemd-run tidak ditemukan."
281
282 local state
283 state="$(systemctl is-system-running 2>/dev/null || true)"
284 case "$state" in
285 running|degraded|starting) ;;
286 *) fatal "Status systemd tidak siap: ${state:-unknown}" ;;
287 esac
288
289 ok "systemd siap (${state})."
290}
291
292acquire_lock() {
293 install -d -m 0755 /run/lock
294 exec 9>"$LOCK_FILE"
295 flock -n "$LOCK_FD" \
296 || fatal "Ada proses instalasi WARP IPv4 lain yang sedang berjalan."
297}
298
299bootstrap_dependencies() {
300 local -a required=(apt-get apt-cache dpkg dpkg-query systemctl systemd-run flock)
301 local cmd
302
303 for cmd in "${required[@]}"; do
304 command -v "$cmd" >/dev/null 2>&1 || fatal "Perintah wajib tidak ditemukan: $cmd"
305 done
306
307 if (( CHECK_ONLY == 1 )); then
308 local -a check_commands=(ip curl gpg getent timeout awk grep sed find tar df install)
309 local -a missing=()
310 for cmd in "${check_commands[@]}"; do
311 command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
312 done
313 ((${#missing[@]} == 0)) \
314 || fatal "Mode --check-only tidak memasang paket. Perintah yang belum ada: ${missing[*]}"
315 return 0
316 fi
317
318 log "Tahap 1/6: memastikan dependensi dasar tersedia melalui IPv6"
319 export DEBIAN_FRONTEND=noninteractive
320
321 apt-get "${APT_OPTS[@]}" update
322 apt-get "${APT_OPTS[@]}" install -y --no-install-recommends \
323 ca-certificates curl gnupg lsb-release iproute2 util-linux
324
325 for cmd in ip curl gpg getent timeout awk grep sed find tar df install; do
326 command -v "$cmd" >/dev/null 2>&1 || fatal "Dependensi terpasang tetapi perintah tidak ditemukan: $cmd"
327 done
328
329 IP_BIN="$(command -v ip)"
330 ok "Dependensi dasar siap."
331}
332
333detect_interface() {
334 if [[ -n "$IFACE" ]]; then
335 validate_iface "$IFACE" || fatal "Nama interface tidak valid: $IFACE"
336 ip link show dev "$IFACE" >/dev/null 2>&1 \
337 || fatal "Interface $IFACE tidak ditemukan."
338 return 0
339 fi
340
341 IFACE="$(
342 ip -6 route show default 2>/dev/null \
343 | awk '!found {for (i=1; i<=NF; i++) if ($i=="dev") {d=$(i+1); if (d !~ /^(lo|CloudflareWARP|warp|wg|tun)/) {print d; found=1; break}}}'
344 )"
345
346 if [[ -z "$IFACE" ]]; then
347 IFACE="$(
348 ip -6 route get 2606:4700:4700::1111 2>/dev/null \
349 | awk '!found {for (i=1; i<=NF; i++) if ($i=="dev") {print $(i+1); found=1; break}}'
350 )"
351 fi
352
353 if [[ -z "$IFACE" ]]; then
354 IFACE="$(
355 ip -6 -o addr show scope global 2>/dev/null \
356 | awk '!found && $2 !~ /^(lo|CloudflareWARP|warp|wg|tun)/ {print $2; found=1}'
357 )"
358 fi
359
360 [[ -n "$IFACE" ]] \
361 || fatal "Interface IPv6 utama tidak dapat dideteksi. Gunakan IFACE=eth0."
362 validate_iface "$IFACE" || fatal "Nama interface hasil deteksi tidak valid: $IFACE"
363 ip link show dev "$IFACE" >/dev/null 2>&1 \
364 || fatal "Interface hasil deteksi tidak ada: $IFACE"
365}
366
367read_state_value() {
368 local key="$1"
369 local file="$2"
370
371 awk -F= -v wanted="$key" '
372 $1 == wanted && !found {
373 value = substr($0, index($0, "=") + 1)
374 if (value !~ /[[:space:]]/) {
375 print value
376 found = 1
377 }
378 }
379 ' "$file" 2>/dev/null
380}
381
382read_old_managed_state() {
383 local unit_line
384
385 if [[ -r "$STATE_FILE" ]]; then
386 OLD_IFACE="$(read_state_value IFACE "$STATE_FILE")"
387 OLD_DUMMY_IPV4="$(read_state_value DUMMY_IPV4 "$STATE_FILE")"
388 OLD_DUMMY_PREFIX="$(read_state_value DUMMY_PREFIX "$STATE_FILE")"
389 OLD_DUMMY_GATEWAY="$(read_state_value DUMMY_GATEWAY "$STATE_FILE")"
390 OLD_DUMMY_METRIC="$(read_state_value DUMMY_METRIC "$STATE_FILE")"
391 fi
392
393 if [[ -r "$BOOTSTRAP_UNIT" ]]; then
394 while IFS= read -r unit_line; do
395 if [[ -z "$OLD_DUMMY_IPV4" \
396 && "$unit_line" =~ ^ExecStart=.*[/]ip[[:space:]]+-4[[:space:]]+addr[[:space:]]+replace[[:space:]]+([0-9.]+)/([0-9]+)[[:space:]]+dev[[:space:]]+([a-zA-Z0-9_.:-]+)$ ]]; then
397 OLD_DUMMY_IPV4="${BASH_REMATCH[1]}"
398 OLD_DUMMY_PREFIX="${BASH_REMATCH[2]}"
399 OLD_IFACE="${BASH_REMATCH[3]}"
400 fi
401
402 if [[ -z "$OLD_DUMMY_GATEWAY" \
403 && "$unit_line" =~ ^ExecStart=.*[/]ip[[:space:]]+-4[[:space:]]+route[[:space:]]+replace[[:space:]]+default[[:space:]]+via[[:space:]]+([0-9.]+)[[:space:]]+dev[[:space:]]+([a-zA-Z0-9_.:-]+).*metric[[:space:]]+([0-9]+)$ ]]; then
404 OLD_DUMMY_GATEWAY="${BASH_REMATCH[1]}"
405 [[ -n "$OLD_IFACE" ]] || OLD_IFACE="${BASH_REMATCH[2]}"
406 OLD_DUMMY_METRIC="${BASH_REMATCH[3]}"
407 fi
408 done <"$BOOTSTRAP_UNIT"
409 fi
410
411 if [[ -n "$OLD_IFACE" ]] && ! validate_iface "$OLD_IFACE"; then
412 OLD_IFACE=""
413 fi
414 if [[ -n "$OLD_DUMMY_IPV4" ]] && ! validate_ipv4 "$OLD_DUMMY_IPV4"; then
415 OLD_DUMMY_IPV4=""
416 fi
417 if [[ -n "$OLD_DUMMY_GATEWAY" ]] && ! validate_ipv4 "$OLD_DUMMY_GATEWAY"; then
418 OLD_DUMMY_GATEWAY=""
419 fi
420 if [[ -n "$OLD_DUMMY_PREFIX" && ! "$OLD_DUMMY_PREFIX" =~ ^[0-9]+$ ]]; then
421 OLD_DUMMY_PREFIX=""
422 fi
423 if [[ -n "$OLD_DUMMY_METRIC" && ! "$OLD_DUMMY_METRIC" =~ ^[0-9]+$ ]]; then
424 OLD_DUMMY_METRIC=""
425 fi
426}
427
428is_managed_ipv4_address() {
429 local iface="$1"
430 local cidr="$2"
431
432 [[ "$iface" == "$IFACE" && "$cidr" == "${DUMMY_IPV4}/${DUMMY_PREFIX}" ]] \
433 && return 0
434
435 [[ -n "$OLD_IFACE" && -n "$OLD_DUMMY_IPV4" && -n "$OLD_DUMMY_PREFIX" \
436 && "$iface" == "$OLD_IFACE" \
437 && "$cidr" == "${OLD_DUMMY_IPV4}/${OLD_DUMMY_PREFIX}" ]] \
438 && return 0
439
440 [[ "$iface" =~ ^CloudflareWARP ]] && return 0
441 return 1
442}
443
444is_managed_default_route() {
445 local route="$1"
446
447 [[ "$route" == *"via ${DUMMY_GATEWAY} dev ${IFACE}"* ]] && return 0
448
449 [[ -n "$OLD_DUMMY_GATEWAY" && -n "$OLD_IFACE" \
450 && "$route" == *"via ${OLD_DUMMY_GATEWAY} dev ${OLD_IFACE}"* ]] \
451 && return 0
452
453 [[ "$route" =~ dev[[:space:]]+CloudflareWARP ]] && return 0
454 return 1
455}
456
457check_ipv6_only_target() {
458 local iface cidr route
459 local found_unmanaged=0
460
461 while IFS=' ' read -r iface cidr; do
462 [[ -n "$iface" && -n "$cidr" ]] || continue
463 if ! is_managed_ipv4_address "$iface" "$cidr"; then
464 warn "IPv4 global tidak dikelola script: ${cidr} pada ${iface}"
465 found_unmanaged=1
466 fi
467 done < <(ip -4 -o addr show scope global 2>/dev/null | awk '{print $2, $4}')
468
469 while IFS= read -r route; do
470 [[ -n "$route" ]] || continue
471 if ! is_managed_default_route "$route"; then
472 warn "Default route IPv4 tidak dikelola script: ${route}"
473 found_unmanaged=1
474 fi
475 done < <(ip -4 route show default 2>/dev/null)
476
477 ((found_unmanaged == 0)) \
478 || fatal "Host memiliki IPv4 asli/route IPv4 lain. Script ini hanya untuk VPS IPv6-only."
479
480 ok "Tidak ditemukan IPv4 native yang akan bentrok."
481}
482
483check_ssh_safety() {
484 local client_ip="" client_port="" server_ip="" server_port=""
485
486 if [[ -n "${SSH_CONNECTION:-}" ]]; then
487 IFS=' ' read -r client_ip client_port server_ip server_port <<<"$SSH_CONNECTION"
488 if [[ "$server_ip" == *:* ]]; then
489 ok "Sesi SSH terdeteksi melalui IPv6 (${server_ip})."
490 elif (( ALLOW_IPV4_SSH == 1 )); then
491 warn "Sesi SSH terlihat memakai IPv4 (${server_ip}); ALLOW_IPV4_SSH=1 aktif."
492 else
493 fatal "Sesi SSH terlihat memakai IPv4 (${server_ip}). Gunakan console/noVNC atau set ALLOW_IPV4_SSH=1 dengan risiko putus koneksi."
494 fi
495 else
496 warn "SSH_CONNECTION tidak tersedia. Pastikan console/noVNC tetap terbuka selama instalasi."
497 fi
498}
499
500check_ipv6_host_state() {
501 local route6 global_v6 disable_all disable_iface free_kb
502
503 global_v6="$(
504 ip -6 -o addr show dev "$IFACE" scope global 2>/dev/null \
505 | awk '!found {print $4; found=1}'
506 )"
507 [[ -n "$global_v6" ]] \
508 || fatal "Interface ${IFACE} tidak memiliki IPv6 global."
509
510 route6="$(ip -6 route get 2606:4700:4700::1111 2>/dev/null || true)"
511 [[ -n "$route6" ]] || fatal "Tidak ada route IPv6 ke Internet."
512 grep -Eq "dev[[:space:]]+${IFACE}([[:space:]]|$)" <<<"$route6" \
513 || fatal "Route IPv6 utama tidak melalui ${IFACE}: ${route6}"
514
515 ip -6 route get 2606:4700:102::3 >/dev/null 2>&1 \
516 || fatal "Tidak ada route IPv6 ke endpoint WARP 2606:4700:102::3."
517
518 disable_all="$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null || echo 1)"
519 disable_iface="$(cat "/proc/sys/net/ipv6/conf/${IFACE}/disable_ipv6" 2>/dev/null || echo 1)"
520 [[ "$disable_all" == "0" && "$disable_iface" == "0" ]] \
521 || fatal "IPv6 dinonaktifkan melalui sysctl pada host/interface."
522
523 free_kb="$(df -Pk / | awk 'NR==2 {print $4}')"
524 [[ "$free_kb" =~ ^[0-9]+$ ]] || fatal "Ruang disk bebas tidak dapat dibaca."
525 ((free_kb >= 307200)) \
526 || fatal "Ruang disk bebas kurang dari 300 MiB (${free_kb} KiB)."
527
528 ok "IPv6 native aktif pada ${IFACE}: ${global_v6}"
529 ok "Ruang disk memadai: $((free_kb / 1024)) MiB bebas."
530}
531
532check_tun_capability() {
533 [[ -c /dev/net/tun ]] \
534 || fatal "/dev/net/tun tidak tersedia. Aktifkan TUN/TAP dari panel provider VPS."
535 [[ -r /dev/net/tun && -w /dev/net/tun ]] \
536 || fatal "/dev/net/tun tidak dapat diakses baca/tulis oleh root."
537
538 if (( CHECK_ONLY == 1 )); then
539 ok "Device TUN tersedia (uji pembuatan interface dilewati pada --check-only)."
540 return 0
541 fi
542
543 TEMP_TUN="wpft$((RANDOM % 10000))"
544 if ! ip tuntap add dev "$TEMP_TUN" mode tun >/dev/null 2>&1; then
545 TEMP_TUN=""
546 fatal "TUN ada tetapi kernel/container menolak pembuatan interface. Periksa CAP_NET_ADMIN/provider VPS."
547 fi
548 ip link delete "$TEMP_TUN" >/dev/null 2>&1 \
549 || fatal "Interface TUN preflight tidak dapat dibersihkan."
550 TEMP_TUN=""
551
552 ok "TUN dan CAP_NET_ADMIN siap."
553}
554
555curl_ipv6_probe() {
556 local url="$1"
557 timeout 30 curl -6 -fsS \
558 --connect-timeout 10 \
559 --max-time 25 \
560 --retry 2 \
561 --retry-delay 2 \
562 "$url" >/dev/null
563}
564
565check_ipv6_dns_https() {
566 local http_code
567
568 getent ahostsv6 pkg.cloudflareclient.com 2>/dev/null | grep -q ':' \
569 || fatal "DNS tidak mengembalikan AAAA untuk pkg.cloudflareclient.com."
570 getent ahostsv6 engage.cloudflareclient.com 2>/dev/null | grep -q ':' \
571 || fatal "DNS tidak mengembalikan AAAA untuk engage.cloudflareclient.com."
572
573 curl_ipv6_probe "https://www.cloudflare.com/cdn-cgi/trace" \
574 || fatal "HTTPS IPv6 ke Cloudflare tidak berfungsi."
575 curl_ipv6_probe "https://pkg.cloudflareclient.com/" \
576 || fatal "Repository Cloudflare tidak dapat diakses melalui IPv6."
577
578 http_code="$(
579 timeout 30 curl -6 -sS -o /dev/null -w '%{http_code}' \
580 --connect-timeout 10 --max-time 25 \
581 https://engage.cloudflareclient.com/ 2>/dev/null \
582 || true
583 )"
584 [[ -n "$http_code" && "$http_code" != "000" ]] \
585 || fatal "Endpoint engage.cloudflareclient.com tidak dapat dijangkau melalui IPv6/TCP 443."
586
587 ok "DNS dan HTTPS IPv6 ke Cloudflare/repository berfungsi."
588}
589
590prepare_repository_and_package() {
591 local key_ascii key_gpg source_tmp candidate policy_output policy_rc
592 local -a reinstall_opt=()
593
594 log "Tahap 2/6: memvalidasi repository dan paket WARP sebelum pembersihan"
595 TMP_DIR="$(mktemp -d /tmp/warp-ipv4-egress.XXXXXX)"
596 key_ascii="${TMP_DIR}/cloudflare-pubkey.gpg"
597 key_gpg="${TMP_DIR}/cloudflare-keyring.gpg"
598 source_tmp="${TMP_DIR}/cloudflare-client.list"
599
600 timeout 45 curl -6 -fsSL \
601 --connect-timeout 10 \
602 --max-time 40 \
603 --retry 3 \
604 --retry-delay 2 \
605 https://pkg.cloudflareclient.com/pubkey.gpg \
606 -o "$key_ascii" \
607 || fatal "GPG key Cloudflare gagal diunduh melalui IPv6."
608
609 gpg --batch --yes --dearmor --output "$key_gpg" "$key_ascii" \
610 || fatal "GPG key Cloudflare gagal diproses."
611 [[ -s "$key_gpg" ]] || fatal "Keyring Cloudflare kosong."
612
613 printf 'deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ %s main\n' \
614 "$VERSION_CODENAME" >"$source_tmp"
615
616 install -m 0644 "$key_gpg" /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
617 install -m 0644 "$source_tmp" /etc/apt/sources.list.d/cloudflare-client.list
618
619 export DEBIAN_FRONTEND=noninteractive
620 apt-get "${APT_OPTS[@]}" update \
621 || fatal "apt-get update repository gagal melalui IPv6."
622 ok "Indeks repository selesai diperbarui."
623
624 policy_output=""
625 policy_rc=0
626 set +e
627 policy_output="$(apt-cache policy cloudflare-warp 2>&1)"
628 policy_rc=$?
629 set -e
630
631 if ((policy_rc != 0)); then
632 printf '%s\n' "$policy_output" >&2
633 fatal "apt-cache policy cloudflare-warp gagal (exit ${policy_rc})."
634 fi
635
636 candidate="$(
637 awk '$1 == "Candidate:" && !found {print $2; found=1}' \
638 <<<"$policy_output"
639 )"
640 [[ -n "$candidate" && "$candidate" != "(none)" ]] \
641 || {
642 printf '%s\n' "$policy_output" >&2
643 fatal "Paket cloudflare-warp tidak tersedia untuk ${VERSION_CODENAME}/${ARCH}."
644 }
645
646 ok "Kandidat paket WARP ditemukan: ${candidate}"
647
648 if dpkg-query -W -f='${Status}' cloudflare-warp 2>/dev/null \
649 | grep -q '^install ok installed$'; then
650 reinstall_opt=(--reinstall)
651 fi
652
653 apt-get "${APT_OPTS[@]}" install -y --download-only --no-install-recommends \
654 "${reinstall_opt[@]}" cloudflare-warp \
655 || fatal "Paket/dependensi WARP tidak dapat diunduh penuh melalui IPv6."
656
657 ok "Paket WARP kandidat ${candidate} tersedia dan dapat diunduh melalui IPv6."
658}
659
660backup_existing_state() {
661 local timestamp
662 local -a paths=()
663 local -a rel_paths=()
664 local path
665
666 for path in \
667 "$BOOTSTRAP_UNIT" \
668 "$BOOTSTRAP_HELPER" \
669 "$WARP_DROPIN_FILE" \
670 "$LEGACY_DROPIN_FILE" \
671 "$STATE_DIR" \
672 /etc/cloudflare-warp \
673 /var/lib/cloudflare-warp \
674 /etc/apt/sources.list.d/cloudflare-client.list \
675 /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg; do
676 [[ -e "$path" || -L "$path" ]] && paths+=("$path")
677 done
678
679 ((${#paths[@]} > 0)) || return 0
680
681 install -d -m 0700 "$BACKUP_DIR"
682 timestamp="$(date '+%Y%m%d-%H%M%S')"
683 BACKUP_FILE="${BACKUP_DIR}/preinstall-${timestamp}.tar.gz"
684
685 for path in "${paths[@]}"; do
686 rel_paths+=("${path#/}")
687 done
688
689 umask 077
690 tar -C / -czf "$BACKUP_FILE" --ignore-failed-read "${rel_paths[@]}" \
691 || fatal "Backup state lama gagal dibuat."
692 chmod 0600 "$BACKUP_FILE"
693 ok "Backup konfigurasi/state lama: ${BACKUP_FILE}"
694}
695
696stop_warp_safely() {
697 if command -v warp-cli >/dev/null 2>&1; then
698 timeout 10 warp-cli --accept-tos disconnect >/dev/null 2>&1 \
699 || timeout 10 warp-cli disconnect >/dev/null 2>&1 \
700 || true
701 fi
702
703 systemctl stop warp-svc.service >/dev/null 2>&1 || true
704
705 local i
706 for i in {1..10}; do
707 systemctl is-active --quiet warp-svc.service || break
708 sleep 1
709 done
710
711 if systemctl is-active --quiet warp-svc.service; then
712 systemctl kill --kill-who=all --signal=KILL warp-svc.service >/dev/null 2>&1 || true
713 systemctl stop warp-svc.service >/dev/null 2>&1 || true
714 fi
715}
716
717mask_warp_service() {
718 stop_warp_safely
719 systemctl unmask warp-svc.service >/dev/null 2>&1 || true
720 systemctl mask --runtime warp-svc.service >/dev/null \
721 || fatal "warp-svc gagal di-mask sementara."
722 WARP_MASKED_BY_SCRIPT=1
723}
724
725unmask_warp_service() {
726 systemctl unmask --runtime warp-svc.service >/dev/null 2>&1 || true
727 systemctl unmask warp-svc.service >/dev/null 2>&1 || true
728 WARP_MASKED_BY_SCRIPT=0
729}
730
731remove_route_and_address() {
732 local iface="$1"
733 local address="$2"
734 local prefix="$3"
735 local gateway="$4"
736 local metric="$5"
737
738 [[ -n "$iface" ]] || return 0
739 validate_iface "$iface" || return 0
740 ip link show dev "$iface" >/dev/null 2>&1 || return 0
741
742 if [[ -n "$gateway" ]] && validate_ipv4 "$gateway"; then
743 ip -4 route del default via "$gateway" dev "$iface" metric "${metric:-65000}" >/dev/null 2>&1 \
744 || ip -4 route del default via "$gateway" dev "$iface" >/dev/null 2>&1 \
745 || true
746 fi
747
748 if [[ -n "$address" && -n "$prefix" ]] \
749 && validate_ipv4 "$address" \
750 && [[ "$prefix" =~ ^[0-9]+$ ]]; then
751 ip -4 addr del "${address}/${prefix}" dev "$iface" >/dev/null 2>&1 || true
752 fi
753}
754
755cleanup_old_rollback_units() {
756 systemctl stop \
757 "${LEGACY_ROLLBACK_UNIT}.timer" \
758 "${LEGACY_ROLLBACK_UNIT}.service" \
759 >/dev/null 2>&1 || true
760 systemctl reset-failed \
761 "${LEGACY_ROLLBACK_UNIT}.timer" \
762 "${LEGACY_ROLLBACK_UNIT}.service" \
763 >/dev/null 2>&1 || true
764 rm -f "$ROLLBACK_SCRIPT"
765}
766
767remove_managed_artifacts() {
768 systemctl disable --now "$BOOTSTRAP_SERVICE" >/dev/null 2>&1 || true
769
770 remove_route_and_address \
771 "$IFACE" "$DUMMY_IPV4" "$DUMMY_PREFIX" "$DUMMY_GATEWAY" "$DUMMY_METRIC"
772
773 if [[ -n "$OLD_IFACE" ]]; then
774 remove_route_and_address \
775 "$OLD_IFACE" "$OLD_DUMMY_IPV4" "$OLD_DUMMY_PREFIX" \
776 "$OLD_DUMMY_GATEWAY" "$OLD_DUMMY_METRIC"
777 fi
778
779 rm -f "$BOOTSTRAP_UNIT" "$BOOTSTRAP_HELPER" "$WARP_DROPIN_FILE" "$LEGACY_DROPIN_FILE"
780 rmdir "$WARP_DROPIN_DIR" >/dev/null 2>&1 || true
781 rm -rf "$STATE_DIR"
782
783 ip link delete CloudflareWARP >/dev/null 2>&1 || true
784 rm -rf /run/cloudflare-warp
785
786 systemctl daemon-reload
787 systemctl reset-failed warp-svc.service "$BOOTSTRAP_SERVICE" >/dev/null 2>&1 || true
788}
789
790fresh_cleanup() {
791 log "Tahap 3/6: membersihkan instalasi lama agar benar-benar fresh"
792 backup_existing_state
793 cleanup_old_rollback_units
794 mask_warp_service
795 remove_managed_artifacts
796
797 if dpkg-query -W -f='${Status}' cloudflare-warp 2>/dev/null \
798 | grep -q '^install ok installed$'; then
799 apt-get "${APT_OPTS[@]}" purge -y cloudflare-warp \
800 || fatal "Paket cloudflare-warp lama gagal di-purge."
801 fi
802
803 rm -rf /etc/cloudflare-warp /var/lib/cloudflare-warp /var/log/cloudflare-warp /run/cloudflare-warp
804 systemctl daemon-reload
805 systemctl reset-failed warp-svc.service >/dev/null 2>&1 || true
806
807 if dpkg-query -W -f='${Status}' cloudflare-warp 2>/dev/null \
808 | grep -q '^install ok installed$'; then
809 fatal "Paket cloudflare-warp masih terpasang setelah purge."
810 fi
811
812 [[ ! -e /var/lib/cloudflare-warp ]] \
813 || fatal "State lama /var/lib/cloudflare-warp belum bersih."
814
815 ok "Paket, registrasi, runtime, unit custom, route dummy, dan state lama sudah dibersihkan."
816}
817
818prepare_nonfresh_upgrade() {
819 log "Tahap 3/6: menyiapkan upgrade tanpa menghapus registrasi WARP"
820 backup_existing_state
821 cleanup_old_rollback_units
822 mask_warp_service
823 remove_managed_artifacts
824 ok "Registrasi/state WARP dipertahankan; unit dan route bootstrap lama dibersihkan."
825}
826
827install_warp_package() {
828 log "Tahap 4/6: memasang paket Cloudflare WARP dengan daemon tetap ditahan"
829 export DEBIAN_FRONTEND=noninteractive
830
831 apt-get "${APT_OPTS[@]}" install -y --no-install-recommends cloudflare-warp \
832 || fatal "Instalasi paket cloudflare-warp gagal."
833
834 dpkg-query -W -f='${Status}' cloudflare-warp 2>/dev/null \
835 | grep -q '^install ok installed$' \
836 || fatal "dpkg tidak menandai cloudflare-warp sebagai paket terpasang."
837
838 command -v warp-cli >/dev/null 2>&1 || fatal "warp-cli tidak ditemukan setelah instalasi."
839 command -v warp-svc >/dev/null 2>&1 || fatal "warp-svc tidak ditemukan setelah instalasi."
840
841 unmask_warp_service
842 systemctl daemon-reload
843 systemctl cat warp-svc.service >/dev/null 2>&1 \
844 || fatal "Unit warp-svc.service tidak ditemukan setelah instalasi."
845
846 init_warp_cli
847 check_cli_capabilities
848
849 ok "Paket terpasang: $(warp-cli --version 2>/dev/null || echo unknown)"
850}
851
852init_warp_cli() {
853 if warp-cli --help 2>&1 | grep -q -- '--accept-tos'; then
854 WARP_ACCEPT_TOS=1
855 else
856 WARP_ACCEPT_TOS=0
857 fi
858}
859
860warp_cli() {
861 if (( WARP_ACCEPT_TOS == 1 )); then
862 command warp-cli --accept-tos "$@"
863 else
864 command warp-cli "$@"
865 fi
866}
867
868warp_cli_timeout() {
869 local duration="$1"
870 shift
871
872 if (( WARP_ACCEPT_TOS == 1 )); then
873 timeout "$duration" warp-cli --accept-tos "$@"
874 else
875 timeout "$duration" warp-cli "$@"
876 fi
877}
878
879check_cli_capabilities() {
880 local help_text
881
882 help_text="$(warp-cli registration --help 2>&1 || true)"
883 grep -Eq '(^|[[:space:]])new([[:space:]]|$)' <<<"$help_text" \
884 || fatal "Versi warp-cli tidak menyediakan 'registration new'."
885
886 help_text="$(warp-cli tunnel --help 2>&1 || true)"
887 grep -Eq '(^|[[:space:]])protocol([[:space:]]|$)' <<<"$help_text" \
888 || fatal "Versi warp-cli tidak menyediakan pengaturan tunnel protocol."
889 grep -Eq '(^|[[:space:]])ip([[:space:]]|$)' <<<"$help_text" \
890 || fatal "Versi warp-cli tidak menyediakan pengaturan split tunnel IP."
891
892 help_text="$(warp-cli tunnel ip --help 2>&1 || true)"
893 grep -q 'add-range' <<<"$help_text" \
894 || fatal "Versi warp-cli tidak menyediakan 'tunnel ip add-range'."
895
896 ok "Kapabilitas CLI yang dibutuhkan tersedia."
897}
898
899arm_rollback() {
900 cat >"$ROLLBACK_SCRIPT" <<EOF_ROLLBACK
901#!/usr/bin/env bash
902set +e
903timeout 8 warp-cli --accept-tos disconnect >/dev/null 2>&1 || timeout 8 warp-cli disconnect >/dev/null 2>&1
904systemctl disable --now warp-svc.service >/dev/null 2>&1
905systemctl disable --now ${BOOTSTRAP_SERVICE} >/dev/null 2>&1
906${IP_BIN} -4 route del default via ${DUMMY_GATEWAY} dev ${IFACE} metric ${DUMMY_METRIC} >/dev/null 2>&1
907${IP_BIN} -4 route del default via ${DUMMY_GATEWAY} dev ${IFACE} >/dev/null 2>&1
908${IP_BIN} -4 addr del ${DUMMY_IPV4}/${DUMMY_PREFIX} dev ${IFACE} >/dev/null 2>&1
909rm -f ${BOOTSTRAP_UNIT} ${BOOTSTRAP_HELPER} ${WARP_DROPIN_FILE}
910rmdir ${WARP_DROPIN_DIR} >/dev/null 2>&1
911rm -rf ${STATE_DIR} /run/cloudflare-warp
912systemctl daemon-reload >/dev/null 2>&1
913systemctl restart ssh.service >/dev/null 2>&1 || systemctl restart sshd.service >/dev/null 2>&1
914EOF_ROLLBACK
915 chmod 0700 "$ROLLBACK_SCRIPT"
916
917 systemd-run \
918 --quiet \
919 --unit="$ROLLBACK_UNIT" \
920 --on-active=10m \
921 --timer-property=AccuracySec=1s \
922 /bin/bash "$ROLLBACK_SCRIPT" \
923 || fatal "Timer rollback keselamatan gagal dibuat."
924
925 ROLLBACK_ARMED=1
926 warn "Rollback keselamatan aktif selama 10 menit. Pertahankan console/noVNC sampai verifikasi selesai."
927}
928
929write_bootstrap_service() {
930 log "Tahap 5/6: membuat bootstrap IPv4 dan memulai daemon secara terurut"
931
932 install -d -m 0755 "$(dirname "$BOOTSTRAP_HELPER")"
933 cat >"$BOOTSTRAP_HELPER" <<EOF_HELPER
934#!/usr/bin/env bash
935set -eu
936case "\${1:-}" in
937 start)
938 ${IP_BIN} -4 addr replace ${DUMMY_IPV4}/${DUMMY_PREFIX} dev ${IFACE}
939 ${IP_BIN} -4 route replace default via ${DUMMY_GATEWAY} dev ${IFACE} onlink metric ${DUMMY_METRIC}
940 ;;
941 stop)
942 ${IP_BIN} -4 route del default via ${DUMMY_GATEWAY} dev ${IFACE} metric ${DUMMY_METRIC} >/dev/null 2>&1 \
943 || ${IP_BIN} -4 route del default via ${DUMMY_GATEWAY} dev ${IFACE} >/dev/null 2>&1 \
944 || true
945 ${IP_BIN} -4 addr del ${DUMMY_IPV4}/${DUMMY_PREFIX} dev ${IFACE} >/dev/null 2>&1 || true
946 ;;
947 *)
948 echo "usage: \$0 {start|stop}" >&2
949 exit 2
950 ;;
951esac
952EOF_HELPER
953 chmod 0755 "$BOOTSTRAP_HELPER"
954
955 cat >"$BOOTSTRAP_UNIT" <<EOF_UNIT
956[Unit]
957Description=IPv4 bootstrap for Cloudflare WARP on an IPv6-only VPS
958Wants=network-online.target
959After=network-online.target
960Before=warp-svc.service
961ConditionPathExists=/sys/class/net/${IFACE}
962
963[Service]
964Type=oneshot
965RemainAfterExit=yes
966ExecStart=${BOOTSTRAP_HELPER} start
967ExecStop=${BOOTSTRAP_HELPER} stop
968
969[Install]
970WantedBy=multi-user.target
971EOF_UNIT
972
973 install -d -m 0755 "$WARP_DROPIN_DIR"
974 cat >"$WARP_DROPIN_FILE" <<EOF_DROPIN
975[Unit]
976Requires=${BOOTSTRAP_SERVICE}
977After=${BOOTSTRAP_SERVICE}
978EOF_DROPIN
979
980 rm -f "$LEGACY_DROPIN_FILE"
981
982 install -d -m 0700 "$STATE_DIR"
983 cat >"$STATE_FILE" <<EOF_STATE
984IFACE=${IFACE}
985DUMMY_IPV4=${DUMMY_IPV4}
986DUMMY_PREFIX=${DUMMY_PREFIX}
987DUMMY_GATEWAY=${DUMMY_GATEWAY}
988DUMMY_METRIC=${DUMMY_METRIC}
989EOF_STATE
990 chmod 0600 "$STATE_FILE"
991
992 systemctl daemon-reload
993 systemctl enable "$BOOTSTRAP_SERVICE" warp-svc.service >/dev/null
994 systemctl restart "$BOOTSTRAP_SERVICE"
995
996 ip -4 addr show dev "$IFACE" \
997 | grep -Fq "inet ${DUMMY_IPV4}/${DUMMY_PREFIX}" \
998 || fatal "IPv4 dummy gagal dipasang pada ${IFACE}."
999
1000 ip -4 route show default \
1001 | grep -Fq "default via ${DUMMY_GATEWAY} dev ${IFACE}" \
1002 || fatal "Default route IPv4 dummy gagal dibuat."
1003
1004 verify_native_ipv6_route
1005 ok "IPv4 dummy aktif: ${DUMMY_IPV4}/${DUMMY_PREFIX}, gateway ${DUMMY_GATEWAY}, metric ${DUMMY_METRIC}."
1006}
1007
1008verify_native_ipv6_route() {
1009 local route6
1010 route6="$(ip -6 route get 2606:4700:4700::1111 2>/dev/null || true)"
1011 [[ -n "$route6" ]] || fatal "Route IPv6 hilang setelah perubahan IPv4."
1012 grep -Eq "dev[[:space:]]+${IFACE}([[:space:]]|$)" <<<"$route6" \
1013 || fatal "IPv6 tidak lagi melalui ${IFACE}: ${route6}"
1014}
1015
1016ipc_output_is_usable() {
1017 local output="$1"
1018 [[ -n "$output" ]] || return 1
1019
1020 if grep -Eqi \
1021 'unable to connect.*daemon|daemon.*not running|connection refused|no such file or directory|ipc.*(failed|error)' \
1022 <<<"$output"; then
1023 return 1
1024 fi
1025
1026 return 0
1027}
1028
1029start_daemon_and_wait_for_ipc() {
1030 local i output rc restarted=0
1031
1032 rm -rf /run/cloudflare-warp
1033 install -d -m 0755 -o root -g root /run/cloudflare-warp
1034
1035 systemctl reset-failed warp-svc.service >/dev/null 2>&1 || true
1036 systemctl start warp-svc.service \
1037 || fatal "warp-svc.service gagal dimulai."
1038
1039 for i in {1..60}; do
1040 output=""
1041 rc=0
1042 set +e
1043 output="$(warp_cli_timeout 6 status 2>&1)"
1044 rc=$?
1045 set -e
1046
1047 if systemctl is-active --quiet warp-svc.service \
1048 && ((rc != 124 && rc != 137)) \
1049 && ipc_output_is_usable "$output"; then
1050 ok "Daemon WARP aktif dan IPC/CLI merespons."
1051 return 0
1052 fi
1053
1054 if ((i == 30 && restarted == 0)); then
1055 warn "IPC belum siap setelah 30 detik; membersihkan runtime lalu restart daemon satu kali."
1056 stop_warp_safely
1057 rm -rf /run/cloudflare-warp
1058 install -d -m 0755 -o root -g root /run/cloudflare-warp
1059 systemctl start warp-svc.service || true
1060 restarted=1
1061 fi
1062
1063 sleep 1
1064 done
1065
1066 printf '%s\n' "${output:-warp-cli tidak menghasilkan output}" >&2
1067 fatal "Daemon WARP aktif tetapi IPC/CLI tidak siap setelah 60 detik."
1068}
1069
1070registration_is_present() {
1071 local output rc
1072 output=""
1073 rc=0
1074
1075 set +e
1076 output="$(warp_cli_timeout 20 registration show 2>&1)"
1077 rc=$?
1078 set -e
1079
1080 ((rc == 0)) || return 1
1081 grep -Eqi 'not registered|registration.*(missing|not found)|no registration|error:' <<<"$output" \
1082 && return 1
1083 [[ -n "$output" ]]
1084}
1085
1086create_registration_once() {
1087 if (( WARP_ACCEPT_TOS == 1 )); then
1088 warp_cli_timeout 90 registration new
1089 else
1090 printf 'y\n' | timeout 90 warp-cli registration new
1091 fi
1092}
1093
1094ensure_registration() {
1095 local attempt rc output
1096
1097 if registration_is_present; then
1098 ok "Registrasi WARP sudah tersedia."
1099 return 0
1100 fi
1101
1102 warn "Registrasi WARP belum tersedia; membuat registrasi consumer baru."
1103
1104 for attempt in 1 2 3; do
1105 output=""
1106 rc=0
1107 set +e
1108 output="$(create_registration_once 2>&1)"
1109 rc=$?
1110 set -e
1111 [[ -n "$output" ]] && printf '%s\n' "$output"
1112
1113 if registration_is_present; then
1114 ok "Registrasi WARP berhasil dibuat pada percobaan ${attempt}."
1115 return 0
1116 fi
1117
1118 if ((attempt < 3)); then
1119 warn "Registrasi belum tervalidasi (rc=${rc}); restart daemon dan mencoba lagi."
1120 stop_warp_safely
1121 rm -rf /run/cloudflare-warp
1122 install -d -m 0755 -o root -g root /run/cloudflare-warp
1123 systemctl start warp-svc.service || true
1124 start_daemon_and_wait_for_ipc
1125 fi
1126 done
1127
1128 fatal "Registrasi WARP gagal setelah 3 percobaan. Rilis Linux terbaru memiliki known issue IPC; reboot server lalu jalankan ulang script dapat diperlukan."
1129}
1130
1131configure_warp() {
1132 local ip_list
1133
1134 warp_cli_timeout 20 mode warp \
1135 || fatal "Gagal mengatur WARP ke traffic-only mode."
1136 warp_cli_timeout 20 tunnel protocol set MASQUE \
1137 || fatal "Gagal mengatur protokol MASQUE."
1138
1139 ip_list="$(warp_cli_timeout 20 tunnel ip list 2>&1 || true)"
1140 if grep -Eqi 'include mode|included routes' <<<"$ip_list"; then
1141 fatal "Split Tunnel terdeteksi dalam Include mode; script tidak akan mengubahnya karena berisiko mengalihkan IPv6."
1142 fi
1143
1144 if ! grep -Eq '(^|[[:space:]])::/0([[:space:]]|$)' <<<"$ip_list"; then
1145 warp_cli_timeout 20 tunnel ip add-range ::/0 \
1146 || fatal "Gagal menambahkan ::/0 ke Split Tunnel Exclude."
1147 fi
1148
1149 ip_list="$(warp_cli_timeout 20 tunnel ip list 2>&1 || true)"
1150 grep -Eq '(^|[[:space:]])::/0([[:space:]]|$)' <<<"$ip_list" \
1151 || fatal "::/0 tidak ditemukan setelah konfigurasi Split Tunnel."
1152
1153 verify_native_ipv6_route
1154 ok "WARP disetel ke traffic-only/MASQUE dan semua IPv6 (::/0) dikecualikan."
1155}
1156
1157status_is_connected() {
1158 local output="$1"
1159
1160 grep -Eqi 'disconnected|not[[:space:]]+connected|unable|failed|error' <<<"$output" \
1161 && return 1
1162
1163 grep -Eqi '(^|[^[:alpha:]])Connected([^[:alpha:]]|$)' <<<"$output"
1164}
1165
1166wait_until_connected() {
1167 local i status rc
1168
1169 set +e
1170 warp_cli_timeout 25 connect
1171 rc=$?
1172 set -e
1173 if ((rc != 0)); then
1174 warn "Perintah connect mengembalikan rc=${rc}; status akan tetap dipantau."
1175 fi
1176
1177 status=""
1178 for i in {1..90}; do
1179 set +e
1180 status="$(warp_cli_timeout 8 status 2>&1)"
1181 rc=$?
1182 set -e
1183
1184 if ((rc != 124 && rc != 137)) && status_is_connected "$status"; then
1185 printf '%s\n' "$status"
1186 ok "WARP tersambung."
1187 return 0
1188 fi
1189 sleep 1
1190 done
1191
1192 printf '%s\n' "${status:-warp-cli status kosong}" >&2
1193 fatal "WARP tidak mencapai status Connected dalam 90 detik."
1194}
1195
1196verify_routes_and_connectivity() {
1197 local trace4 trace6 route6 service_enabled bootstrap_enabled
1198
1199 trace4="$(
1200 timeout 35 curl -4 -fsS \
1201 --connect-timeout 12 --max-time 30 \
1202 https://www.cloudflare.com/cdn-cgi/trace
1203 )" || fatal "Tes HTTPS IPv4 melalui WARP gagal."
1204
1205 grep -q '^warp=on$' <<<"$trace4" \
1206 || fatal "IPv4 berhasil keluar tetapi trace tidak menunjukkan warp=on."
1207
1208 route6="$(ip -6 route get 2606:4700:4700::1111 2>/dev/null || true)"
1209 grep -Eq "dev[[:space:]]+${IFACE}([[:space:]]|$)" <<<"$route6" \
1210 || fatal "Route IPv6 tidak lagi melalui ${IFACE}."
1211
1212 trace6="$(
1213 timeout 35 curl -6 -fsS \
1214 --connect-timeout 12 --max-time 30 \
1215 https://www.cloudflare.com/cdn-cgi/trace
1216 )" || fatal "Tes HTTPS IPv6 native gagal setelah WARP aktif."
1217
1218 grep -q '^warp=off$' <<<"$trace6" \
1219 || fatal "Trace IPv6 tidak menunjukkan warp=off; pengecualian ::/0 belum aman."
1220
1221 systemctl is-active --quiet warp-svc.service \
1222 || fatal "warp-svc tidak aktif setelah verifikasi."
1223 systemctl is-active --quiet "$BOOTSTRAP_SERVICE" \
1224 || fatal "Bootstrap IPv4 tidak aktif setelah verifikasi."
1225
1226 service_enabled="$(systemctl is-enabled warp-svc.service 2>/dev/null || true)"
1227 bootstrap_enabled="$(systemctl is-enabled "$BOOTSTRAP_SERVICE" 2>/dev/null || true)"
1228 [[ "$service_enabled" == "enabled" ]] \
1229 || fatal "warp-svc belum enabled untuk boot."
1230 [[ "$bootstrap_enabled" == "enabled" ]] \
1231 || fatal "Bootstrap IPv4 belum enabled untuk boot."
1232
1233 printf '\n--- IPv4 trace ---\n'
1234 grep -E '^(ip|colo|loc|warp)=' <<<"$trace4"
1235 printf '\n--- IPv6 trace ---\n'
1236 grep -E '^(ip|colo|loc|warp)=' <<<"$trace6"
1237 printf '\n--- IPv6 route ---\n%s\n' "$route6"
1238
1239 ok "IPv4 keluar melalui WARP; IPv6 dan SSH tetap pada interface native ${IFACE}."
1240}
1241
1242disarm_rollback() {
1243 systemctl stop "${ROLLBACK_UNIT}.timer" "${ROLLBACK_UNIT}.service" >/dev/null 2>&1 || true
1244 systemctl reset-failed "${ROLLBACK_UNIT}.timer" "${ROLLBACK_UNIT}.service" >/dev/null 2>&1 || true
1245 rm -f "$ROLLBACK_SCRIPT"
1246 ROLLBACK_ARMED=0
1247}
1248
1249print_preflight_summary() {
1250 cat <<EOF_SUMMARY
1251
1252============================================================
1253 PREFLIGHT LULUS — belum ada instalasi/pembersihan dilakukan
1254============================================================
1255OS : ${PRETTY_NAME} (${VERSION_CODENAME})
1256Arsitektur : ${ARCH}
1257Interface IPv6 : ${IFACE}
1258IPv4 dummy target : ${DUMMY_IPV4}/${DUMMY_PREFIX}
1259Dummy gateway : ${DUMMY_GATEWAY} (metric ${DUMMY_METRIC})
1260Fresh install : ${FRESH_INSTALL}
1261============================================================
1262EOF_SUMMARY
1263}
1264
1265print_summary() {
1266 local public_v6 package_version
1267
1268 public_v6="$(
1269 ip -6 -o addr show dev "$IFACE" scope global 2>/dev/null \
1270 | awk '!found {print $4; found=1}'
1271 )"
1272 package_version="$(dpkg-query -W -f='${Version}' cloudflare-warp 2>/dev/null || echo unknown)"
1273
1274 cat <<EOF_SUMMARY
1275
1276============================================================
1277 Cloudflare WARP IPv4 egress berhasil dipasang
1278============================================================
1279Script : ${SCRIPT_VERSION}
1280Paket WARP : ${package_version}
1281Interface : ${IFACE}
1282IPv6 asli : ${public_v6:-unknown}
1283IPv4 dummy : ${DUMMY_IPV4}/${DUMMY_PREFIX}
1284Dummy gateway : ${DUMMY_GATEWAY} (metric ${DUMMY_METRIC})
1285WARP mode : traffic-only (warp)
1286Tunnel : MASQUE
1287IPv6 split route : ::/0 excluded
1288Fresh install : ${FRESH_INSTALL}
1289Backup lama : ${BACKUP_FILE:-tidak ada}
1290
1291Cek status:
1292 warp-cli --accept-tos status
1293 curl -4 -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|warp)='
1294 curl -6 -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|warp)='
1295
1296Catatan:
1297 - IPv4 WARP hanya untuk koneksi keluar.
1298 - SSH dan trafik IPv6 tetap menggunakan IPv6 asli VPS.
1299 - Jalankan dari console/noVNC saat melakukan perubahan routing berikutnya.
1300============================================================
1301EOF_SUMMARY
1302}
1303
1304run_full_preflight() {
1305 log "Menjalankan preflight host IPv6-only"
1306 detect_interface
1307 IP_BIN="$(command -v ip)"
1308 read_old_managed_state
1309 check_ssh_safety
1310 check_ipv6_only_target
1311 check_ipv6_host_state
1312 check_tun_capability
1313 check_ipv6_dns_https
1314 ok "Semua pemeriksaan host dan konektivitas lulus."
1315}
1316
1317main() {
1318 parse_args "$@"
1319 require_root
1320 check_inputs
1321 check_platform
1322 check_systemd
1323 acquire_lock
1324 bootstrap_dependencies
1325 run_full_preflight
1326
1327 if (( CHECK_ONLY == 1 )); then
1328 print_preflight_summary
1329 INSTALL_SUCCESS=1
1330 return 0
1331 fi
1332
1333 prepare_repository_and_package
1334
1335 if (( FRESH_INSTALL == 1 )); then
1336 fresh_cleanup
1337 else
1338 prepare_nonfresh_upgrade
1339 fi
1340
1341 detect_interface
1342 check_ipv6_host_state
1343 install_warp_package
1344
1345 arm_rollback
1346 write_bootstrap_service
1347 start_daemon_and_wait_for_ipc
1348 ensure_registration
1349 configure_warp
1350
1351 log "Tahap 6/6: menghubungkan dan memverifikasi jalur IPv4/IPv6"
1352 wait_until_connected
1353 verify_routes_and_connectivity
1354 disarm_rollback
1355 print_summary
1356
1357 INSTALL_SUCCESS=1
1358}
1359
1360main "$@"
1361