-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtailscale-manager.sh
More file actions
executable file
·1022 lines (918 loc) · 33.7 KB
/
Copy pathtailscale-manager.sh
File metadata and controls
executable file
·1022 lines (918 loc) · 33.7 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
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/sh
# Interactive script for installing, updating, and managing Tailscale on OpenWRT
# https://github.com/fl0w1nd/openwrt-tailscale
# shellcheck disable=SC2034
set -e
normalize_base_url() {
printf '%s' "${1%/}"
}
derive_small_api_base_url() {
local base_url="$1"
local scheme host path
scheme=$(printf '%s' "$base_url" | sed -n 's#^\([A-Za-z][A-Za-z0-9+.-]*\)://.*#\1#p')
host=$(printf '%s' "$base_url" | sed -n 's#^[A-Za-z][A-Za-z0-9+.-]*://\([^/]*\)/.*#\1#p')
path=$(printf '%s' "$base_url" | sed -n 's#^[A-Za-z][A-Za-z0-9+.-]*://[^/]*/\(.*\)$#\1#p')
[ -n "$scheme" ] || return 1
[ -n "$host" ] || return 1
[ -n "$path" ] || return 1
case "$host" in
github.com)
printf '%s://api.github.com/repos/%s' "$scheme" "$path"
;;
*)
printf '%s://%s/api/v3/repos/%s' "$scheme" "$host" "$path"
;;
esac
}
# ============================================================================
# Configuration
# ============================================================================
VERSION="4.6.2"
# Download source: "official" or "small"
# - official: Full binaries from pkgs.tailscale.com (~30-35MB)
# - small: Compressed binaries from GitHub releases (~8-10 MB)
DOWNLOAD_SOURCE="${TAILSCALE_SOURCE:-official}"
# Official Tailscale download
DEFAULT_TAILSCALE_OFFICIAL_BASE_URL="https://pkgs.tailscale.com/stable"
TAILSCALE_OFFICIAL_BASE_URL=$(normalize_base_url "${TAILSCALE_OFFICIAL_BASE_URL:-$DEFAULT_TAILSCALE_OFFICIAL_BASE_URL}")
API_URL="${TAILSCALE_OFFICIAL_BASE_URL}/?mode=json"
DOWNLOAD_BASE="$TAILSCALE_OFFICIAL_BASE_URL"
OFFICIAL_VERSIONS_URL="${TAILSCALE_OFFICIAL_BASE_URL}/#static"
# Small binary download (GitHub releases)
SMALL_REPO="fl0w1nd/openwrt-tailscale"
DEFAULT_TAILSCALE_SMALL_BASE_URL="https://github.com/${SMALL_REPO}"
TAILSCALE_SMALL_BASE_URL=$(normalize_base_url "${TAILSCALE_SMALL_BASE_URL:-$DEFAULT_TAILSCALE_SMALL_BASE_URL}")
SMALL_API_BASE_URL=$(derive_small_api_base_url "$TAILSCALE_SMALL_BASE_URL")
SMALL_API_URL="${SMALL_API_BASE_URL}/releases/latest"
SMALL_RELEASES_API="${SMALL_API_BASE_URL}/releases"
SMALL_DOWNLOAD_BASE="${TAILSCALE_SMALL_BASE_URL}/releases/download"
# Architectures supported by small binaries
# Must match the architectures built in GitHub Actions
SMALL_SUPPORTED_ARCHS="amd64 arm64 arm armv6 armv5 mipsle mips"
# Installation paths
# PERSISTENT_DIR can be overridden via env var, e.g. to point at an external
# mount such as a USB stick on devices with limited internal flash.
PERSISTENT_DIR="${PERSISTENT_DIR:-/opt/tailscale}"
RAM_DIR="/tmp/tailscale"
STATE_FILE="/etc/config/tailscaled.state"
STATE_DIR="/etc/tailscale"
CONFIG_FILE="/etc/config/tailscale"
INIT_SCRIPT="/etc/init.d/tailscale"
CRON_SCRIPT="/usr/bin/tailscale-update"
# LOG_FILE can be overridden via env var, e.g. to keep the manager log on
# persistent storage (the default /var/log is usually tmpfs and is wiped on
# reboot, which makes after-the-fact troubleshooting harder).
LOG_FILE="${LOG_FILE:-/var/log/tailscale-manager.log}"
MANAGER_BIN_PATH="${TAILSCALE_MANAGER_BIN_PATH:-/usr/bin/tailscale-manager}"
# Project repository raw base URL
DEFAULT_REPO_BASE_URL="https://raw.githubusercontent.com/${SMALL_REPO}/main"
REPO_BASE_URL="${OPENWRT_TAILSCALE_REPO_BASE_URL:-$DEFAULT_REPO_BASE_URL}"
DEFAULT_MGMT_BASE_URL="https://raw.githubusercontent.com/${SMALL_REPO}/mgmt"
MGMT_BASE_URL="${OPENWRT_TAILSCALE_MGMT_BASE_URL:-$DEFAULT_MGMT_BASE_URL}"
MANAGER_SCRIPT_URL="${REPO_BASE_URL}/tailscale-manager.sh"
MGMT_VERSION_URL="${MGMT_BASE_URL}/latest/VERSION"
MGMT_BUNDLE_URL="${MGMT_BASE_URL}/latest/tailscale-mgmt.tar.gz"
MGMT_BUNDLE_SHA256_URL="${MGMT_BASE_URL}/latest/tailscale-mgmt.tar.gz.sha256"
CONFIG_TEMPLATE_URL="${REPO_BASE_URL}/etc/config/tailscale"
INIT_SCRIPT_URL="${REPO_BASE_URL}/etc/init.d/tailscale"
UPDATE_SCRIPT_URL="${REPO_BASE_URL}/usr/bin/tailscale-update"
COMMON_LIB_URL="${REPO_BASE_URL}/usr/lib/tailscale/common.sh"
COMMON_LIB_PATH="/usr/lib/tailscale/common.sh"
# LuCI app file URLs
LUCI_VIEW_BASE_URL="${REPO_BASE_URL}/luci-app-tailscale/htdocs/luci-static/resources/view/tailscale"
LUCI_RPC_URL="${REPO_BASE_URL}/luci-app-tailscale/root/usr/libexec/rpcd/luci-tailscale"
LUCI_MENU_URL="${REPO_BASE_URL}/luci-app-tailscale/root/usr/share/luci/menu.d/luci-app-tailscale.json"
LUCI_ACL_URL="${REPO_BASE_URL}/luci-app-tailscale/root/usr/share/rpcd/acl.d/luci-app-tailscale.json"
# LuCI app destination paths (overridable for testing)
LUCI_VIEW_DIR="${LUCI_VIEW_DIR:-/www/luci-static/resources/view/tailscale}"
LUCI_RPC_DEST="${LUCI_RPC_DEST:-/usr/libexec/rpcd/luci-tailscale}"
LUCI_MENU_DEST="${LUCI_MENU_DEST:-/usr/share/luci/menu.d/luci-app-tailscale.json}"
LUCI_ACL_DEST="${LUCI_ACL_DEST:-/usr/share/rpcd/acl.d/luci-app-tailscale.json}"
# Module library directory (overridable for testing)
LIB_DIR="${LIB_DIR:-/usr/lib/tailscale}"
# Module libraries sourced from $LIB_DIR
MODULE_LIBS="jsonutil.sh version.sh download.sh firewall.sh deploy.sh selfupdate.sh commands.sh menu.sh json.sh"
# ============================================================================
# Logging Functions
# ============================================================================
log_file() {
local level="$1"
local msg="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
}
log_info() {
echo "[INFO] $1"
logger -t "tailscale-manager" -p daemon.info "$1"
log_file "INFO" "$1"
}
log_error() {
echo "[ERROR] $1" >&2
logger -t "tailscale-manager" -p daemon.error "$1"
log_file "ERROR" "$1"
}
log_warn() {
echo "[WARN] $1"
logger -t "tailscale-manager" -p daemon.warn "$1"
log_file "WARN" "$1"
}
# General-purpose file downloader (kept in entry script for bootstrap)
download_repo_file() {
local url="$1"
local dest="$2"
local mode="${3:-644}"
local dest_dir dest_name tmp_file
dest_dir=$(dirname "$dest")
dest_name=$(basename "$dest")
mkdir -p "$dest_dir"
tmp_file=$(mktemp "${dest_dir}/.${dest_name}.XXXXXX" 2>/dev/null) || {
log_error "Failed to create temporary file for ${dest}"
return 1
}
if ! wget -qO "$tmp_file" "$url" 2>/dev/null; then
rm -f "$tmp_file"
log_error "Failed to download ${url}"
return 1
fi
if [ ! -s "$tmp_file" ]; then
rm -f "$tmp_file"
log_error "Downloaded file is empty: ${url}"
return 1
fi
if ! mv "$tmp_file" "$dest"; then
rm -f "$tmp_file"
log_error "Failed to install ${dest}"
return 1
fi
chmod "$mode" "$dest" 2>/dev/null || true
return 0
}
# ============================================================================
# Shared Function Library
# ============================================================================
# Functions shared with init script and other components.
# Canonical versions live in /usr/lib/tailscale/common.sh.
# Inline fallback below is used during bootstrap (before first install).
# This copy must stay in sync with common.sh — enforced by `make check-sync`.
if [ -f "$COMMON_LIB_PATH" ]; then
# shellcheck source=/dev/null
. "$COMMON_LIB_PATH"
else
# shellcheck disable=SC2120
get_openwrt_arch() {
local root="${1:-}"
local arch=""
if [ -r "${root}/etc/openwrt_release" ]; then
arch=$(grep -E '^DISTRIB_ARCH=' "${root}/etc/openwrt_release" 2>/dev/null \
| head -n1 | cut -d= -f2- | tr -d "'\"")
fi
if [ -z "$arch" ] && [ -r "${root}/etc/apk/arch" ]; then
arch=$(head -n1 "${root}/etc/apk/arch" 2>/dev/null)
fi
if [ -z "$arch" ] && [ -r "${root}/etc/opkg.conf" ]; then
arch=$(awk '/^[[:space:]]*arch[[:space:]]/ {
if ($2 != "all" && $2 != "noarch") { print $2; exit }
}' "${root}/etc/opkg.conf" 2>/dev/null)
fi
printf '%s' "$arch"
}
get_arch() {
local arch owrt_arch
local result=""
arch=$(uname -m)
case "$arch" in
x86_64)
result="amd64"
;;
aarch64)
result="arm64"
;;
armv7l|armv7)
if grep -q 'vfpv3\|vfpv4\|vfpd32' /proc/cpuinfo 2>/dev/null; then
result="arm"
elif grep -q 'vfp' /proc/cpuinfo 2>/dev/null; then
result="armv6"
else
result="armv5"
fi
;;
armv6l|armv6)
result="armv6"
;;
armv5tel|armv5tejl|armv5l|armv5)
result="armv5"
;;
mipsel)
result="mipsle"
;;
mips)
# shellcheck disable=SC2119
owrt_arch=$(get_openwrt_arch)
case "$owrt_arch" in
mipsel*) result="mipsle" ;;
mips_*) result="mips" ;;
*)
if grep -q "little endian" /proc/cpuinfo 2>/dev/null; then
result="mipsle"
elif grep -q "big endian" /proc/cpuinfo 2>/dev/null; then
result="mips"
else
result="mips"
fi
;;
esac
;;
mips64el)
result="mips64le"
;;
mips64)
# shellcheck disable=SC2119
owrt_arch=$(get_openwrt_arch)
case "$owrt_arch" in
mips64el*|mipsel*) result="mips64le" ;;
mips64_*|mips_*) result="mips64" ;;
*)
if grep -q "little endian" /proc/cpuinfo 2>/dev/null; then
result="mips64le"
elif grep -q "big endian" /proc/cpuinfo 2>/dev/null; then
result="mips64"
else
result="mips64"
fi
;;
esac
;;
i686|i386)
result="386"
;;
riscv64)
result="riscv64"
;;
*)
return 1
;;
esac
echo "$result"
}
ensure_tun_device_node() {
if [ ! -e "/dev/net/tun" ]; then
mkdir -p /dev/net
mknod /dev/net/tun c 10 200 2>/dev/null || true
chmod 666 /dev/net/tun 2>/dev/null || true
fi
}
kernel_has_builtin_tun() {
if [ -r /proc/config.gz ]; then
zcat /proc/config.gz 2>/dev/null | grep -q '^CONFIG_TUN=y$'
elif [ -r "/boot/config-$(uname -r)" ]; then
grep -q '^CONFIG_TUN=y$' "/boot/config-$(uname -r)" 2>/dev/null
else
return 1
fi
}
kernel_tun_available() {
if [ -d "/sys/module/tun" ]; then
ensure_tun_device_node
[ -e "/dev/net/tun" ]
return $?
fi
modprobe tun 2>/dev/null || insmod tun 2>/dev/null || true
if [ -d "/sys/module/tun" ]; then
ensure_tun_device_node
[ -e "/dev/net/tun" ]
return $?
fi
if kernel_has_builtin_tun; then
ensure_tun_device_node
[ -e "/dev/net/tun" ]
return $?
fi
return 1
}
migrate_config() {
command -v uci >/dev/null 2>&1 || return 0
local old_val
old_val="$(uci -q get tailscale.settings.tun_mode)" || return 0
if ! uci -q get tailscale.settings.net_mode >/dev/null 2>&1; then
uci set tailscale.settings.net_mode="$old_val"
fi
uci delete tailscale.settings.tun_mode 2>/dev/null || true
uci commit tailscale 2>/dev/null || true
}
get_effective_net_mode() {
local requested_mode="${1:-auto}"
case "$requested_mode" in
userspace)
echo "userspace"
return 0
;;
tun|kernel)
kernel_tun_available && echo "tun"
return $?
;;
auto|"")
if kernel_tun_available; then
echo "tun"
else
echo "userspace"
fi
return 0
;;
*)
if kernel_tun_available; then
echo "tun"
else
echo "userspace"
fi
return 0
;;
esac
}
validate_version_format() {
case "$1" in
''|.*|*.|*..*|*[!0-9.]*)
return 1
;;
*.*)
return 0
;;
*)
return 1
;;
esac
}
fi
# ============================================================================
# Module Libraries
# ============================================================================
# Module libraries provide version, download, firewall, deploy, and
# selfupdate functions. They are installed to $LIB_DIR by
# install_runtime_scripts(). During bootstrap (first install), they may not
# exist yet — the _ensure_libraries() function handles downloading them.
for _lib in $MODULE_LIBS; do
if [ -f "$LIB_DIR/$_lib" ]; then
# shellcheck source=/dev/null
. "$LIB_DIR/$_lib"
fi
done
unset _lib
# Bootstrap helper: download and source module libraries if missing.
# Called by main() before dispatching commands that need them.
_ensure_libraries() {
mkdir -p "$LIB_DIR"
local _lib
local _missing=0
for _lib in $MODULE_LIBS; do
if [ ! -f "$LIB_DIR/$_lib" ]; then
_missing=1
break
fi
done
[ "$_missing" -eq 0 ] && return 0
for _lib in $MODULE_LIBS; do
download_repo_file "${REPO_BASE_URL}/usr/lib/tailscale/${_lib}" "${LIB_DIR}/${_lib}" 644 || return 1
done
for _lib in $MODULE_LIBS; do
[ -f "$LIB_DIR/$_lib" ] || {
log_error "Missing module library after bootstrap: ${LIB_DIR}/${_lib}"
return 1
}
# shellcheck source=/dev/null
. "$LIB_DIR/$_lib"
done
log_info "Bootstrapped runtime libraries into ${LIB_DIR}"
}
# ============================================================================
# Dependency Management
# ============================================================================
check_dependencies() {
log_info "Checking system dependencies..."
local has_opkg=0
local deps_to_install=""
local need_update=0
local warn_count=0
if command -v opkg >/dev/null 2>&1; then
has_opkg=1
fi
# --- wget ---
if ! command -v wget >/dev/null 2>&1; then
warn_count=$((warn_count + 1))
log_warn "wget not found (required for downloading binaries)"
if [ "$has_opkg" = "1" ]; then
deps_to_install="$deps_to_install wget-ssl"
need_update=1
else
log_warn " Please install wget manually: opkg install wget-ssl"
fi
fi
# --- HTTPS support (libustream-*) ---
if ! wget -q --spider "$TAILSCALE_OFFICIAL_BASE_URL" 2>/dev/null; then
if command -v wget >/dev/null 2>&1; then
warn_count=$((warn_count + 1))
log_warn "HTTPS support may be missing (wget cannot reach https endpoint)"
if [ "$has_opkg" = "1" ]; then
if opkg list-installed 2>/dev/null | grep -q "libustream-mbedtls\|libustream-openssl\|libustream-wolfssl"; then
: # already installed, likely a transient network issue
else
deps_to_install="$deps_to_install libustream-mbedtls"
need_update=1
fi
else
log_warn " Please install SSL support: opkg install libustream-mbedtls"
fi
fi
fi
# --- CA certificates ---
if [ ! -f "/etc/ssl/certs/ca-certificates.crt" ]; then
if [ "$has_opkg" = "0" ] || ! opkg list-installed 2>/dev/null | grep -q "ca-bundle"; then
warn_count=$((warn_count + 1))
log_warn "ca-bundle is missing (needed for HTTPS verification)"
if [ "$has_opkg" = "1" ]; then
deps_to_install="$deps_to_install ca-bundle"
need_update=1
else
log_warn " Please install ca-bundle manually: opkg install ca-bundle"
fi
fi
fi
# --- kmod-tun ---
if [ ! -d "/sys/module/tun" ]; then
modprobe tun 2>/dev/null || insmod tun 2>/dev/null || true
fi
if [ ! -d "/sys/module/tun" ] && ! kernel_has_builtin_tun; then
warn_count=$((warn_count + 1))
log_warn "kmod-tun is missing (TUN device not available)"
if [ "$has_opkg" = "1" ]; then
deps_to_install="$deps_to_install kmod-tun"
need_update=1
else
log_warn " Tailscale will fall back to userspace networking mode"
log_warn " To use kernel TUN mode, install kmod-tun manually"
fi
fi
# --- iptables ---
if ! command -v iptables >/dev/null 2>&1; then
warn_count=$((warn_count + 1))
log_warn "iptables command is missing"
if [ "$has_opkg" = "1" ]; then
if [ -x /sbin/fw4 ]; then
deps_to_install="$deps_to_install iptables-nft"
else
deps_to_install="$deps_to_install iptables"
fi
need_update=1
fi
fi
# --- Try auto-install via opkg ---
if [ -n "$deps_to_install" ] && [ "$has_opkg" = "1" ]; then
log_info "Attempting to install missing dependencies:$deps_to_install ..."
if [ "$need_update" -eq 1 ]; then
log_info "Running opkg update..."
opkg update >/dev/null 2>&1 || log_warn "opkg update failed, trying install anyway"
fi
local dep
for dep in $deps_to_install; do
if opkg install "$dep" >/dev/null 2>&1; then
log_info "Installed $dep"
else
log_warn "Failed to install $dep (non-fatal, continuing)"
fi
done
if echo "$deps_to_install" | grep -q "kmod-tun"; then
modprobe tun 2>/dev/null || insmod tun 2>/dev/null || true
fi
fi
if [ "$has_opkg" = "0" ] && [ "$warn_count" -gt 0 ]; then
log_warn "opkg not available, cannot auto-install dependencies"
fi
if [ "$warn_count" -eq 0 ]; then
log_info "All dependencies are met"
fi
setup_tun_device
return 0
}
setup_tun_device() {
if [ ! -d "/sys/module/tun" ]; then
modprobe tun 2>/dev/null || insmod tun 2>/dev/null || true
fi
local had_tun=0
[ -e "/dev/net/tun" ] && had_tun=1
ensure_tun_device_node
if [ "$had_tun" = "0" ] && [ -e "/dev/net/tun" ]; then
log_info "Created /dev/net/tun device node"
elif [ ! -e "/dev/net/tun" ]; then
log_warn "/dev/net/tun could not be created, tailscaled may fail to start"
log_warn "Please check if your kernel supports TUN/TAP (CONFIG_TUN)"
fi
}
# ============================================================================
# Service Status Helpers
# ============================================================================
get_tailscaled_pid() {
local pids
pids="$(pidof tailscaled 2>/dev/null)" || return 1
[ -n "$pids" ] || return 1
echo "${pids%% *}"
}
is_tailscaled_userspace() {
local pid cmd
pid="$(get_tailscaled_pid)" || return 1
cmd="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null)" || return 1
case " $cmd " in
*" --tun=userspace-networking "*|*" --tun userspace-networking "*)
return 0
;;
esac
return 1
}
is_tailscaled_running() {
pidof tailscaled >/dev/null 2>&1
}
wait_for_tailscaled() {
local timeout="${1:-10}"
local i=0
while [ "$i" -lt "$timeout" ]; do
if is_tailscaled_running; then
sleep 1
if is_tailscaled_running; then
return 0
fi
fi
sleep 1
i=$((i + 1))
done
return 1
}
show_service_status() {
local pid
if is_tailscaled_running; then
pid="$(get_tailscaled_pid 2>/dev/null || true)"
if [ -n "$pid" ]; then
log_info "tailscaled is running (PID: $pid)"
else
log_info "tailscaled is running"
fi
if is_tailscaled_userspace; then
log_info "Active mode: userspace"
else
log_info "Active mode: tun"
fi
else
log_error "tailscaled is not running"
fi
}
# ============================================================================
# Configuration Helpers
# ============================================================================
get_configured_net_mode() {
local net_mode="auto"
if [ -r /lib/functions.sh ] && [ -f "$CONFIG_FILE" ]; then
. /lib/functions.sh
migrate_config
config_load tailscale 2>/dev/null || true
config_get net_mode settings net_mode auto
fi
case "$net_mode" in
kernel) net_mode="tun" ;;
esac
echo "${net_mode:-auto}"
}
get_auto_update_config() {
if [ -f "$CONFIG_FILE" ]; then
. /lib/functions.sh
config_load tailscale
config_get auto_update settings auto_update ""
if [ -n "$auto_update" ]; then
echo "$auto_update"
return 0
fi
fi
if crontab -l 2>/dev/null | grep -Fq "$CRON_SCRIPT"; then
echo "1"
else
echo "0"
fi
}
set_auto_update_config() {
local value="$1"
if ! command -v uci >/dev/null 2>&1; then
log_error "uci not found, cannot update config"
return 1
fi
uci set tailscale.settings.auto_update="$value" 2>/dev/null || {
log_error "Failed to set auto_update in UCI"
return 1
}
uci commit tailscale 2>/dev/null || {
log_error "Failed to commit UCI config"
return 1
}
return 0
}
configure_auto_update() {
local enable="$1"
if [ "$enable" = "1" ]; then
log_info "Enabling auto-updates..."
set_auto_update_config "1" || return 1
install_update_script || return 1
else
log_info "Disabling auto-updates..."
set_auto_update_config "0" || return 1
fi
setup_cron
}
configure_net_mode() {
local mode="$1"
local proxy_listen="${2:-}"
case "$mode" in
auto|tun|userspace) ;;
*)
log_error "Invalid networking mode: $mode"
return 1
;;
esac
if [ ! -f "$CONFIG_FILE" ]; then
log_error "Tailscale is not installed. Run: tailscale-manager install"
return 1
fi
uci set tailscale.settings.net_mode="$mode"
if [ -n "$proxy_listen" ]; then
uci set tailscale.settings.proxy_listen="$proxy_listen"
fi
uci commit tailscale || {
log_error "Failed to save networking mode"
return 1
}
log_info "Networking mode set to: $mode"
[ -n "$proxy_listen" ] && log_info "Proxy listen: $proxy_listen"
install_runtime_scripts || return 1
if [ -x "$INIT_SCRIPT" ]; then
log_info "Restarting Tailscale service..."
"$INIT_SCRIPT" restart 2>/dev/null || {
log_warn "Restart failed. Try manually: $INIT_SCRIPT restart"
return 1
}
if wait_for_tailscaled 10; then
show_service_status
else
log_error "tailscaled failed to start. Check logs: cat /var/log/tailscale.log"
return 1
fi
fi
return 0
}
# ============================================================================
# Main Entry Point
# ============================================================================
main() {
mkdir -p "$(dirname "$LOG_FILE")"
local reexeced="${TAILSCALE_MANAGER_REEXEC:-0}"
# Reject unknown commands before any network or filesystem work, so a typo
# (or a non-existent command such as "update-script") never triggers
# dependency bootstrap or a self-update check as a side effect. Previously
# any unrecognised argument fell through to check_script_update first, which
# made bogus commands look like they "did something".
case "${1:-}" in
install|update|rollback|uninstall|status|logs|diagnostics|doctor|download-only|install-version|list-small-versions|list-official-versions|setup-subnet-routing|self-update|auto-update|net-mode|luci|json-status|json-install-info|json-latest-versions|json-latest-version|json-script-local-info|json-script-info|-h|--help|help|-v|--version|"") ;;
*)
echo "Unknown command: $1"
echo "Run '$0 help' for usage"
exit 1
;;
esac
# Bootstrap: ensure module libraries are available for commands that need them.
# Informational commands (help/version) stay offline and need no libraries.
case "${1:-}" in
-h|--help|help|-v|--version) ;;
*)
_ensure_libraries || {
log_error "Failed to initialize runtime libraries from ${REPO_BASE_URL}/usr/lib/tailscale"
exit 1
}
;;
esac
# No implicit update check here: updating the management layer is an
# explicit action (`self-update`, the interactive-menu reminder, or the
# LuCI button). Unrelated commands stay fully offline.
unset TAILSCALE_MANAGER_REEXEC
# Record state-changing invocations to the manager log so a later
# `diagnostics`/`logs` dump shows which commands ran in what order. This is
# file-only (no console/syslog noise) and skips read-only/JSON commands so
# frequent LuCI polling never floods the log.
case "${1:-}" in
install|update|rollback|uninstall|install-version|download-only|setup-subnet-routing|self-update|auto-update|net-mode|luci)
log_file "INFO" "Command invoked: $0 $*"
;;
esac
case "${1:-}" in
install)
# Bare `install` runs the interactive flow. Any arguments switch to
# the non-interactive path (for LuCI/automation); a leading --yes/-y
# is accepted and stripped so it matches the rest of the CLI.
shift
if [ "$#" -eq 0 ]; then
do_install
else
case "${1:-}" in
--yes|-y) shift ;;
esac
cmd_install "$@"
fi
;;
update)
do_update "$2"
;;
rollback)
do_rollback
;;
uninstall)
do_uninstall "$2"
;;
status)
do_status
;;
logs)
shift
do_logs "$@"
;;
diagnostics|doctor)
shift
do_diagnostics "$@"
;;
download-only)
do_download_only
;;
install-version)
shift
cmd_install_version "$@"
;;
list-small-versions)
list_small_versions "${2:-10}"
;;
list-official-versions)
list_official_versions "${2:-20}"
;;
setup-subnet-routing)
do_setup_subnet_routing
;;
self-update)
if [ "$reexeced" = "1" ]; then
return 0
fi
local rc=0
shift
check_script_update self-update "$@" || rc=$?
case "$rc" in
0) ;;
10)
echo "Already up to date (v${VERSION})."
;;
30) ;;
*)
exit 1
;;
esac
;;
auto-update)
# Scheduled automatic updates of the Tailscale binary (cron).
# Canonical values are enable|disable|status; on/off/1/0 stay
# accepted for backward compatibility. The UCI value remains 0/1.
case "${2:-status}" in
enable|on|1)
configure_auto_update "1"
;;
disable|off|0)
configure_auto_update "0"
;;
reconcile)
setup_cron
;;
status|"")
echo ""
echo "Tailscale auto-update status:"
if [ "$(get_auto_update_config)" = "1" ]; then
if crontab -l 2>/dev/null | grep -Fq "$CRON_SCRIPT"; then
echo " Enabled (cron active)"
else
echo " Enabled (cron missing)"
fi
else
echo " Disabled"
fi
echo ""
;;
*)
echo "Usage: $0 auto-update [enable|disable|status]"
exit 1
;;
esac
;;
net-mode)
case "${2:-status}" in
auto|tun|userspace)
configure_net_mode "$2"
;;
status|"")
echo ""
echo "Networking mode:"
echo " Configured: $(get_configured_net_mode)"
if pgrep -f "tailscaled.*userspace-networking" >/dev/null 2>&1; then
echo " Active: userspace"
elif pgrep -f "tailscaled" >/dev/null 2>&1; then
echo " Active: tun"
else
echo " Active: not running"
fi
echo ""
;;
*)
echo "Usage: $0 net-mode [auto|tun|userspace|status]"
exit 1
;;
esac
;;
luci)
shift
do_luci "$@"
;;
json-status)
cmd_json_status
;;
json-install-info)
cmd_json_install_info
;;
json-latest-versions)
cmd_json_latest_versions
;;
json-latest-version)
cmd_json_latest_version
;;
json-script-local-info)
cmd_json_script_local_info
;;
json-script-info)
cmd_json_script_info
;;
-v|--version)
echo "${VERSION}"
;;
-h|--help|help)
echo "OpenWRT Tailscale Manager v${VERSION}"
echo ""
echo "Usage: $0 [command]"
echo ""
echo "Tailscale commands (manage the Tailscale binary this tool installs):"
echo " install Install Tailscale (interactive)"
echo " install --yes [options] Install Tailscale (non-interactive, for LuCI/automation)"
echo " Add --luci 1 to install the optional LuCI UI"
echo " install-version <ver> Install a specific Tailscale version (non-interactive)"
echo " update [--yes] Update the Tailscale binary to the latest version"
echo " rollback Roll back the Tailscale binary to the previous version"
echo " auto-update <enable|disable|status>"
echo " Schedule automatic Tailscale binary updates (cron)"
echo " status Show Tailscale install and runtime status"
echo " list-small-versions [n] List available small (compressed) Tailscale versions"
echo " list-official-versions [n] List available official Tailscale versions"
echo " download-only Download Tailscale binaries only (for RAM mode)"
echo " setup-subnet-routing Configure interface/firewall for subnet routing"
echo " net-mode <auto|tun|userspace|status>"
echo " Configure Tailscale networking mode"
echo " luci <install|remove|status> Manage the optional LuCI web UI"
echo " uninstall [--yes] Remove Tailscale"
echo ""
echo "Diagnostics & troubleshooting:"
echo " logs [n] [--maskinfo] Show recent manager/service/system logs (default ${TS_LOG_DEFAULT_LINES:-200} lines)"
echo " diagnostics [--maskinfo] Print a full troubleshooting report to paste into a bug report"
echo " (alias: doctor)"
echo ""
echo "Manager command (manages this tool itself, NOT the Tailscale binary):"
echo " self-update [--yes] Reinstall this manager (scripts and enabled LuCI) to the latest version"
echo ""
echo "Other:"
echo " help Show this help"
echo " -v, --version Print the manager version and exit"
echo ""
echo "Note: 'update'/'auto-update' act on the Tailscale binary; 'self-update'"
echo " acts on this manager. They are independent of each other."