最後活躍 1787200824

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

ridzimeko 已修改 1787200824. 還原成這個修訂版本

沒有任何變更

ridzimeko 已修改 1787200366. 還原成這個修訂版本

1 file changed, 1360 insertions

warp-ipv6.sh(檔案已創建)

@@ -0,0 +1,1360 @@
1 + #!/usr/bin/env bash
2 +
3 + set -Eeuo pipefail
4 + IFS=$'\n\t'
5 + export LC_ALL=C
6 +
7 + readonly SCRIPT_NAME="${0##*/}"
8 + readonly SCRIPT_VERSION="2.0.1"
9 + readonly BOOTSTRAP_SERVICE="warp-ipv4-bootstrap.service"
10 + readonly BOOTSTRAP_UNIT="/etc/systemd/system/${BOOTSTRAP_SERVICE}"
11 + readonly BOOTSTRAP_HELPER="/usr/local/libexec/warp-ipv4-bootstrap"
12 + readonly WARP_DROPIN_DIR="/etc/systemd/system/warp-svc.service.d"
13 + readonly WARP_DROPIN_FILE="${WARP_DROPIN_DIR}/20-ipv4-bootstrap.conf"
14 + readonly LEGACY_DROPIN_FILE="${WARP_DROPIN_DIR}/override.conf"
15 + readonly STATE_DIR="/var/lib/warp-ipv4-egress"
16 + readonly STATE_FILE="${STATE_DIR}/state.env"
17 + readonly BACKUP_DIR="/root/warp-ipv4-egress-backups"
18 + readonly ROLLBACK_SCRIPT="/root/warp-ipv4-egress-rollback.sh"
19 + readonly ROLLBACK_UNIT="warp-ipv4-egress-rollback-${$}"
20 + readonly LEGACY_ROLLBACK_UNIT="warp-ipv4-egress-rollback"
21 + readonly LOCK_FILE="/run/lock/warp-ipv4-egress-install.lock"
22 +
23 + DUMMY_IPV4="${DUMMY_IPV4:-192.168.254.2}"
24 + DUMMY_PREFIX="${DUMMY_PREFIX:-32}"
25 + DUMMY_GATEWAY="${DUMMY_GATEWAY:-192.168.254.1}"
26 + DUMMY_METRIC="${DUMMY_METRIC:-65000}"
27 + FRESH_INSTALL="${FRESH_INSTALL:-1}"
28 + ALLOW_IPV4_SSH="${ALLOW_IPV4_SSH:-0}"
29 + CHECK_ONLY=0
30 +
31 + IFACE="${IFACE:-}"
32 + VERSION_CODENAME=""
33 + PRETTY_NAME=""
34 + ARCH=""
35 + IP_BIN=""
36 +
37 + OLD_IFACE=""
38 + OLD_DUMMY_IPV4=""
39 + OLD_DUMMY_PREFIX=""
40 + OLD_DUMMY_GATEWAY=""
41 + OLD_DUMMY_METRIC=""
42 +
43 + TMP_DIR=""
44 + BACKUP_FILE=""
45 + DIAG_FILE=""
46 + TEMP_TUN=""
47 + ROLLBACK_ARMED=0
48 + WARP_MASKED_BY_SCRIPT=0
49 + WARP_ACCEPT_TOS=0
50 + INSTALL_SUCCESS=0
51 + LOCK_FD=9
52 +
53 + APT_OPTS=(
54 + -o Acquire::ForceIPv6=true
55 + -o Acquire::Retries=3
56 + -o Dpkg::Use-Pty=0
57 + )
58 +
59 + usage() {
60 + cat <<EOF_USAGE
61 + ${SCRIPT_NAME} ${SCRIPT_VERSION}
62 +
63 + Cloudflare WARP IPv4 egress untuk VPS Ubuntu IPv6-only.
64 +
65 + Opsi:
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 +
71 + Environment override:
72 + IFACE, DUMMY_IPV4, DUMMY_PREFIX, DUMMY_GATEWAY, DUMMY_METRIC,
73 + FRESH_INSTALL=0|1, ALLOW_IPV4_SSH=0|1
74 + EOF_USAGE
75 + }
76 +
77 + log() {
78 + printf '\n\033[1;34m[%s]\033[0m %s\n' "$(date '+%H:%M:%S')" "$*"
79 + }
80 +
81 + ok() {
82 + printf '\033[1;32m[OK]\033[0m %s\n' "$*"
83 + }
84 +
85 + warn() {
86 + printf '\033[1;33m[WARN]\033[0m %s\n' "$*" >&2
87 + }
88 +
89 + collect_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 +
130 + fatal() {
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 +
137 + on_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 +
155 + on_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 +
182 + trap on_error ERR
183 + trap on_exit EXIT
184 + trap 'exit 130' INT
185 + trap 'exit 143' TERM HUP
186 +
187 + parse_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 +
213 + require_root() {
214 + [[ ${EUID} -eq 0 ]] || fatal "Jalankan script ini sebagai root."
215 + }
216 +
217 + validate_boolean() {
218 + [[ "$1" == "0" || "$1" == "1" ]]
219 + }
220 +
221 + validate_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 +
236 + validate_iface() {
237 + [[ "$1" =~ ^[a-zA-Z0-9_.:-]+$ ]]
238 + }
239 +
240 + check_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 +
255 + check_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 +
275 + check_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 +
292 + acquire_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 +
299 + bootstrap_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 +
333 + detect_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 +
367 + read_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 +
382 + read_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 +
428 + is_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 +
444 + is_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 +
457 + check_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 +
483 + check_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 +
500 + check_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 +
532 + check_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 +
555 + curl_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 +
565 + check_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 +
590 + prepare_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 +
660 + backup_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 +
696 + stop_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 +
717 + mask_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 +
725 + unmask_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 +
731 + remove_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 +
755 + cleanup_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 +
767 + remove_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 +
790 + fresh_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 +
818 + prepare_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 +
827 + install_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 +
852 + init_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 +
860 + warp_cli() {
861 + if (( WARP_ACCEPT_TOS == 1 )); then
862 + command warp-cli --accept-tos "$@"
863 + else
864 + command warp-cli "$@"
865 + fi
866 + }
867 +
868 + warp_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 +
879 + check_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 +
899 + arm_rollback() {
900 + cat >"$ROLLBACK_SCRIPT" <<EOF_ROLLBACK
901 + #!/usr/bin/env bash
902 + set +e
903 + timeout 8 warp-cli --accept-tos disconnect >/dev/null 2>&1 || timeout 8 warp-cli disconnect >/dev/null 2>&1
904 + systemctl disable --now warp-svc.service >/dev/null 2>&1
905 + systemctl 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
909 + rm -f ${BOOTSTRAP_UNIT} ${BOOTSTRAP_HELPER} ${WARP_DROPIN_FILE}
910 + rmdir ${WARP_DROPIN_DIR} >/dev/null 2>&1
911 + rm -rf ${STATE_DIR} /run/cloudflare-warp
912 + systemctl daemon-reload >/dev/null 2>&1
913 + systemctl restart ssh.service >/dev/null 2>&1 || systemctl restart sshd.service >/dev/null 2>&1
914 + EOF_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 +
929 + write_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
935 + set -eu
936 + case "\${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 + ;;
951 + esac
952 + EOF_HELPER
953 + chmod 0755 "$BOOTSTRAP_HELPER"
954 +
955 + cat >"$BOOTSTRAP_UNIT" <<EOF_UNIT
956 + [Unit]
957 + Description=IPv4 bootstrap for Cloudflare WARP on an IPv6-only VPS
958 + Wants=network-online.target
959 + After=network-online.target
960 + Before=warp-svc.service
961 + ConditionPathExists=/sys/class/net/${IFACE}
962 +
963 + [Service]
964 + Type=oneshot
965 + RemainAfterExit=yes
966 + ExecStart=${BOOTSTRAP_HELPER} start
967 + ExecStop=${BOOTSTRAP_HELPER} stop
968 +
969 + [Install]
970 + WantedBy=multi-user.target
971 + EOF_UNIT
972 +
973 + install -d -m 0755 "$WARP_DROPIN_DIR"
974 + cat >"$WARP_DROPIN_FILE" <<EOF_DROPIN
975 + [Unit]
976 + Requires=${BOOTSTRAP_SERVICE}
977 + After=${BOOTSTRAP_SERVICE}
978 + EOF_DROPIN
979 +
980 + rm -f "$LEGACY_DROPIN_FILE"
981 +
982 + install -d -m 0700 "$STATE_DIR"
983 + cat >"$STATE_FILE" <<EOF_STATE
984 + IFACE=${IFACE}
985 + DUMMY_IPV4=${DUMMY_IPV4}
986 + DUMMY_PREFIX=${DUMMY_PREFIX}
987 + DUMMY_GATEWAY=${DUMMY_GATEWAY}
988 + DUMMY_METRIC=${DUMMY_METRIC}
989 + EOF_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 +
1008 + verify_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 +
1016 + ipc_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 +
1029 + start_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 +
1070 + registration_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 +
1086 + create_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 +
1094 + ensure_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 +
1131 + configure_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 +
1157 + status_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 +
1166 + wait_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 +
1196 + verify_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 +
1242 + disarm_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 +
1249 + print_preflight_summary() {
1250 + cat <<EOF_SUMMARY
1251 +
1252 + ============================================================
1253 + PREFLIGHT LULUS — belum ada instalasi/pembersihan dilakukan
1254 + ============================================================
1255 + OS : ${PRETTY_NAME} (${VERSION_CODENAME})
1256 + Arsitektur : ${ARCH}
1257 + Interface IPv6 : ${IFACE}
1258 + IPv4 dummy target : ${DUMMY_IPV4}/${DUMMY_PREFIX}
1259 + Dummy gateway : ${DUMMY_GATEWAY} (metric ${DUMMY_METRIC})
1260 + Fresh install : ${FRESH_INSTALL}
1261 + ============================================================
1262 + EOF_SUMMARY
1263 + }
1264 +
1265 + print_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 + ============================================================
1279 + Script : ${SCRIPT_VERSION}
1280 + Paket WARP : ${package_version}
1281 + Interface : ${IFACE}
1282 + IPv6 asli : ${public_v6:-unknown}
1283 + IPv4 dummy : ${DUMMY_IPV4}/${DUMMY_PREFIX}
1284 + Dummy gateway : ${DUMMY_GATEWAY} (metric ${DUMMY_METRIC})
1285 + WARP mode : traffic-only (warp)
1286 + Tunnel : MASQUE
1287 + IPv6 split route : ::/0 excluded
1288 + Fresh install : ${FRESH_INSTALL}
1289 + Backup lama : ${BACKUP_FILE:-tidak ada}
1290 +
1291 + Cek 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 +
1296 + Catatan:
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 + ============================================================
1301 + EOF_SUMMARY
1302 + }
1303 +
1304 + run_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 +
1317 + main() {
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 +
1360 + main "$@"
上一頁 下一頁