-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathPsiphon-Linux-VPN-Service-Setup.sh
More file actions
2677 lines (2307 loc) · 95.8 KB
/
Psiphon-Linux-VPN-Service-Setup.sh
File metadata and controls
2677 lines (2307 loc) · 95.8 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/bash
# Psiphon Linux VPN Service - Zero Trust Network Security Implementation
# Security-First TUN Interface Setup with Kill Switch
#
# Features:
# - Zero-trust networking model with comprehensive kill switch
# - Native TUN interface support for secure packet tunneling
# - Automated binary verification and secure updates
# - Full IPv4/IPv6 traffic isolation through VPN tunnel
# - DNS leak prevention with secure resolution handling
#
# Security measures:
# - Default-deny firewall policy (fail-closed model)
# - Dedicated non-root user isolation
# - Secure process capability restrictions
# - Comprehensive traffic routing enforcement
set -euo pipefail
IFS=$'\n\t'
readonly INSTALLER_VERSION="1.4.2"
# Security and Configuration Parameters
# These values are critical for the security model - DO NOT MODIFY without understanding implications
readonly PSIPHON_USER="psiphon-user" # Dedicated non-root user for process isolation
readonly PSIPHON_GROUP="psiphon-group" # Restricted group for secure operations
readonly SOCKS_PORT=1081 # Local SOCKS proxy port for tunneled traffic
readonly HTTP_PORT=8081 # Local HTTP proxy port for tunneled traffic
readonly INSTALL_DIR="/opt/psiphon-tun" # Base installation directory with restricted access
readonly PSIPHON_DIR="$INSTALL_DIR/psiphon" # Secure binary and config storage location
readonly PSIPHON_BINARY="$PSIPHON_DIR/psiphon-tunnel-core"
readonly PSIPHON_CONFIG_FILE="$PSIPHON_DIR/psiphon.config"
readonly LOG_FILE="$INSTALL_DIR/psiphon-tun.log"
readonly PSIPHON_LOG_FILE="$INSTALL_DIR/psiphon-core.log"
readonly PSIPHON_SPONSOR_HOMEPAGE_PATH="$INSTALL_DIR/data/ca.psiphon.PsiphonTunnel.tunnel-core/homepage"
readonly LOCK_FILE="/run/psiphon-tun.lock"
readonly PID_FILE="/run/psiphon-tun.pid"
readonly GITHUB_API="https://api.github.com/repos/Psiphon-Labs/psiphon-tunnel-core-binaries"
readonly PSIPHON_BINARY_URL="https://github.com/Psiphon-Labs/psiphon-tunnel-core-binaries/raw/master/linux/psiphon-tunnel-core-x86_64"
readonly SERVICE_CONFIGURE_NAME="psiphon-tun"
readonly SERVICE_BINARY_NAME="psiphon-binary"
readonly SERVICE_HOMEPAGE_MONITOR="psiphon-homepage-monitor"
readonly SERVICE_HOMEPAGE_TRIGGER="psiphon-homepage-trigger"
# Network Security Configuration
readonly TUN_INTERFACE="PsiphonTUN" # Dedicated TUN interface for isolated traffic
readonly TUN_SUBNET="10.200.3.0/24" # IPv4 subnet for tunnel traffic isolation
readonly TUN_IP="10.200.3.1" # IPv4 gateway address for tunnel
readonly TUN_PEER_IP="10.200.3.2" # IPv4 peer address for point-to-point tunnel
readonly TUN_SUBNET6="fd42:42:42::/64" # IPv6 subnet (ULA) for tunnel traffic isolation
readonly TUN_IP6="fd42:42:42::1" # IPv6 gateway address for tunnel
readonly TUN_DNS_SERVERS="8.8.8.8,8.8.4.4" # Google DNS
readonly TUN_DNS_SERVERS6="2001:4860:4860::8888,2001:4860:4860::8844" # Google DNS IPv6
# WARP Integration Configuration
readonly WARP_CLI_PATH="/usr/bin/warp-cli" # Path to WARP CLI executable
readonly WARP_SVC_PROCESS="warp-svc" # WARP service process name
readonly WARP_STATUS_CONNECTED="Status update: Connected" # Expected WARP status when connected
readonly WARP_INTERFACE="CloudflareWARP" # WARP interface name
# Secure fallback for interface selection: default route with non-loopback fallback
TUN_BYPASS_INTERFACE=$(ip -json route get 8.8.8.8 2>/dev/null | jq -r '.[0].dev // empty' ||
ip -json link show | jq -r '.[] | select(.link_type!="loopback") | .ifname' | head -n1)
SERVICE_MODE="false" # Set to true when running as a systemd service
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Logging functions
function log() {
local message="$1"
# We want to avoid errors if date command fails
# shellcheck disable=SC2155
local timestamp=$(date +'%Y-%m-%d %H:%M:%S') || true
echo -e "${BLUE}[$timestamp]${NC} $message"
# We want to avoid errors if log file is not writable
# shellcheck disable=SC2015
[[ -w "$LOG_FILE" || -w "$(dirname "$LOG_FILE")" ]] && echo "[$timestamp] $message" >> "$LOG_FILE" 2>/dev/null || true
}
function error() {
local message="$1"
# We want to avoid errors if date command fails
# shellcheck disable=SC2155
local timestamp=$(date +'%Y-%m-%d %H:%M:%S') || true
echo -e "${RED}[$timestamp][ERROR]${NC} $message" >&2
# We want to avoid errors if log file is not writable
# shellcheck disable=SC2015
[[ -w "$LOG_FILE" || -w "$(dirname "$LOG_FILE")" ]] && echo "[$timestamp] ERROR: $message" >> "$LOG_FILE" 2>/dev/null || true
}
function success() {
local message="$1"
# We want to avoid errors if date command fails
# shellcheck disable=SC2155
local timestamp=$(date +'%Y-%m-%d %H:%M:%S') || true
echo -e "${GREEN}[$timestamp][SUCCESS]${NC} $message"
# We want to avoid errors if log file is not writable
# shellcheck disable=SC2015
[[ -w "$LOG_FILE" || -w "$(dirname "$LOG_FILE")" ]] && echo "[$timestamp] $message" >> "$LOG_FILE" 2>/dev/null || true
}
function warning() {
local message="$1"
# We want to avoid errors if date command fails
# shellcheck disable=SC2155
local timestamp=$(date +'%Y-%m-%d %H:%M:%S') || true
echo -e "${YELLOW}[$timestamp][WARNING]${NC} $message"
# We want to avoid errors if log file is not writable
# shellcheck disable=SC2015
[[ -w "$LOG_FILE" || -w "$(dirname "$LOG_FILE")" ]] && echo "[$timestamp] WARNING: $message" >> "$LOG_FILE" 2>/dev/null || true
}
# Security Validation Functions
# Verify root privileges for secure operations
# Required for network configuration and process management
function check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root or with sudo"
exit 1
fi
}
# Process isolation through file locking
# Prevents race conditions and ensures single instance execution
function acquire_lock() {
if [[ -f "$LOCK_FILE" ]]; then
local lock_pid
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")
if [[ -n "$lock_pid" ]] && kill -0 "$lock_pid" 2>/dev/null; then
error "Another instance is already running (PID: $lock_pid)"
exit 1
else
# Remove stale lock file
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
}
# Check for required tools
function check_dependencies() {
local missing_tools=()
for tool in wget curl unzip ip nft jq dig; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing_tools+=("$tool")
fi
done
if [[ ${#missing_tools[@]} -gt 0 ]]; then
error "Missing required tools: ${missing_tools[*]}"
log "Installing missing tools..."
if command -v apt-get >/dev/null 2>&1; then
# On Debian/Ubuntu, nft comes from the nftables package
local packages_to_install=()
for tool in "${missing_tools[@]}"; do
if [ "$tool" = "nft" ]; then
packages_to_install+=("nftables")
elif [ "$tool" = "dig" ]; then
packages_to_install+=("dnsutils")
else
packages_to_install+=("$tool")
fi
done
apt-get update && apt-get install -y "${packages_to_install[@]}"
elif command -v yum >/dev/null 2>&1; then
# On RHEL/CentOS/Fedora
local packages_to_install=()
for tool in "${missing_tools[@]}"; do
if [ "$tool" = "nft" ]; then
packages_to_install+=("nftables")
elif [ "$tool" = "dig" ]; then
packages_to_install+=("bind-utils")
else
packages_to_install+=("$tool")
fi
done
yum install -y "${packages_to_install[@]}"
elif command -v pacman >/dev/null 2>&1; then
# On Arch Linux
local packages_to_install=()
for tool in "${missing_tools[@]}"; do
if [ "$tool" = "nft" ]; then
packages_to_install+=("nftables")
elif [ "$tool" = "dig" ]; then
packages_to_install+=("bind")
else
packages_to_install+=("$tool")
fi
done
pacman -S --noconfirm "${packages_to_install[@]}"
else
error "Cannot install missing tools. Please install manually: ${missing_tools[*]}"
exit 1
fi
fi
}
# Create user and group
function create_user() {
if ! getent group "$PSIPHON_GROUP" >/dev/null 2>&1; then
log "Creating group $PSIPHON_GROUP..."
groupadd --system "$PSIPHON_GROUP"
fi
if ! getent passwd "$PSIPHON_USER" >/dev/null 2>&1; then
log "Creating user $PSIPHON_USER..."
useradd --system --no-create-home --shell /bin/false \
--home-dir /nonexistent --gid "$PSIPHON_GROUP" "$PSIPHON_USER"
fi
}
# Create directory structure
function create_directories() {
log "Creating directory structure..."
mkdir -p "$INSTALL_DIR" "$PSIPHON_DIR" "$INSTALL_DIR/data"
chown -R "$PSIPHON_USER:$PSIPHON_GROUP" "$INSTALL_DIR"
chmod 755 "$INSTALL_DIR" "$PSIPHON_DIR"
chmod 700 "$INSTALL_DIR/data"
}
# Psiphon version management
function get_latest_psiphon_info() {
local commits_api="$GITHUB_API/commits?path=linux/psiphon-tunnel-core-x86_64&per_page=1"
local latest_commit
if ! latest_commit=$(curl -s --connect-timeout 7 --max-time 60 "$commits_api"); then
error "Failed to fetch commit info from GitHub"
return 1
fi
if [[ -z "$latest_commit" ]] || [[ "$latest_commit" == "null" ]] || ! echo "$latest_commit" | jq empty 2>/dev/null; then
error "Invalid response from GitHub API"
return 1
fi
# We want to avoid errors if jq fails. We check later if commit_message is empty
# shellcheck disable=SC2155
local commit_message=$(echo "$latest_commit" | jq -r '.[0].commit.message' 2>/dev/null || echo "") || true
if [[ -z "$commit_message" ]] || [[ "$commit_message" == "null" ]]; then
error "Failed to parse commit information"
return 1
fi
echo "$commit_message"
}
function get_binary_version_info() {
if [[ ! -f "$PSIPHON_BINARY" ]]; then
echo "|"
return
fi
local version_output
if ! version_output=$(exec runuser -u "$PSIPHON_USER" -- "$PSIPHON_BINARY" -v 2>/dev/null); then
echo "|"
return
fi
# We want to avoid errors if grep or sed fails. It's okay if build_date or revision is empty
# shellcheck disable=SC2155
local revision=$(echo "$version_output" | grep "Revision:" | sed 's/Revision: //' | xargs || echo "")
echo "$revision"
}
# Secure binary download and validation
function download_psiphon() {
local temp_file
temp_file=$(mktemp)
log "Downloading latest Psiphon binary..."
if ! wget -q --connect-timeout 7 --timeout=567 --tries=3 "$PSIPHON_BINARY_URL" -O "$temp_file"; then
rm -f "$temp_file" 2>/dev/null || true
error "Failed to download Psiphon binary"
return 1
fi
# Verify it's a valid binary
if ! file "$temp_file" | grep -q "ELF.*executable"; then
rm -f "$temp_file" 2>/dev/null || true
error "Downloaded file is not a valid Linux executable"
return 1
fi
# Make it executable and test version
chmod +x "$temp_file"
local version_output
if ! version_output=$("$temp_file" -v 2>/dev/null); then
rm -f "$temp_file" 2>/dev/null || true
error "Downloaded binary cannot be executed or is invalid"
return 1
fi
if ! echo "$version_output" | grep -q "Psiphon Console Client"; then
rm -f "$temp_file" 2>/dev/null || true
error "Downloaded binary does not appear to be Psiphon"
return 1
fi
# Extract version info
local build_date revision
build_date=$(echo "$version_output" | grep "Build Date:" | sed 's/Build Date: //' | xargs || echo "")
revision=$(echo "$version_output" | grep "Revision:" | sed 's/Revision: //' | xargs || echo "")
if [[ -z "$build_date" ]] || [[ -z "$revision" ]]; then
rm -f "$temp_file" 2>/dev/null || true
error "Cannot extract version information from binary"
return 1
fi
log "Downloaded binary info:"
log " Build Date: $build_date"
log " Revision: $revision"
# Install the binary securely
# cp -f "$temp_file" "$PSIPHON_BINARY"
# chmod 750 "$PSIPHON_BINARY"
# chown "$PSIPHON_USER:$PSIPHON_GROUP" "$PSIPHON_BINARY"
install -m 750 -o "$PSIPHON_USER" -g "$PSIPHON_GROUP" "$temp_file" "$PSIPHON_BINARY"
# Clean up temp file before successful return
rm -f "$temp_file" 2>/dev/null || true
success "Psiphon binary installed successfully"
}
function check_and_update_psiphon() {
log "Checking for Psiphon updates..."
# Get latest commit info from GitHub
local latest_commit_msg
if ! latest_commit_msg=$(get_latest_psiphon_info); then
warning "Failed to fetch latest Psiphon commit info from GitHub"
fi
# Get current binary info
# We want to avoid errors if get_binary_version_info fails
# We download anyway if we cannot determine current version
# shellcheck disable=SC2155
local current_revision=$(get_binary_version_info) || true
log "Latest revision: $latest_commit_msg"
log "Current binary Revision: $current_revision"
# Check if we need to update
local needs_update=false
if [[ ! -f "$PSIPHON_BINARY" ]]; then
log "Binary not found, downloading..."
needs_update=true
elif [[ -z "$current_revision" ]]; then
log "Cannot determine current version, updating..."
needs_update=true
else
if [[ "$latest_commit_msg" != *"$current_revision"* ]]; then
# If timestamps are close or equal, check if revisions are different
log "Different revision detected, updating..."
needs_update=true
fi
fi
if [[ "$needs_update" == true ]]; then
log "Updating Psiphon binary..."
if download_psiphon; then
success "Psiphon updated successfully"
# Show new version info
local new_info new_build_date new_revision
new_info=$(get_binary_version_info)
new_build_date=$(echo "$new_info" | cut -d'|' -f1)
new_revision=$(echo "$new_info" | cut -d'|' -f2)
log "New version: Build Date: $new_build_date, Revision: $new_revision"
return 0
else
error "Failed to update Psiphon"
return 1
fi
else
log "Psiphon is already up to date"
return 0
fi
}
# Create Psiphon configuration
function create_psiphon_config() {
log "Creating Psiphon configuration..."
# See the AvailableEgressRegions in Psiphon logs for valid region codes
# Example:
# Change to `"EgressRegion": "US",` if you want to force to choose US servers
cat > "$PSIPHON_CONFIG_FILE" << 'EOF'
{
"LocalHttpProxyPort": 8081,
"LocalSocksProxyPort": 1081,
"EgressRegion": "",
"PropagationChannelId": "FFFFFFFFFFFFFFFF",
"RemoteServerListDownloadFilename": "remote_server_list",
"RemoteServerListSignaturePublicKey": "MIICIDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEAt7Ls+/39r+T6zNW7GiVpJfzq/xvL9SBH5rIFnk0RXYEYavax3WS6HOD35eTAqn8AniOwiH+DOkvgSKF2caqk/y1dfq47Pdymtwzp9ikpB1C5OfAysXzBiwVJlCdajBKvBZDerV1cMvRzCKvKwRmvDmHgphQQ7WfXIGbRbmmk6opMBh3roE42KcotLFtqp0RRwLtcBRNtCdsrVsjiI1Lqz/lH+T61sGjSjQ3CHMuZYSQJZo/KrvzgQXpkaCTdbObxHqb6/+i1qaVOfEsvjoiyzTxJADvSytVtcTjijhPEV6XskJVHE1Zgl+7rATr/pDQkw6DPCNBS1+Y6fy7GstZALQXwEDN/qhQI9kWkHijT8ns+i1vGg00Mk/6J75arLhqcodWsdeG/M/moWgqQAnlZAGVtJI1OgeF5fsPpXu4kctOfuZlGjVZXQNW34aOzm8r8S0eVZitPlbhcPiR4gT/aSMz/wd8lZlzZYsje/Jr8u/YtlwjjreZrGRmG8KMOzukV3lLmMppXFMvl4bxv6YFEmIuTsOhbLTwFgh7KYNjodLj/LsqRVfwz31PgWQFTEPICV7GCvgVlPRxnofqKSjgTWI4mxDhBpVcATvaoBl1L/6WLbFvBsoAUBItWwctO2xalKxF5szhGm8lccoc5MZr8kfE0uxMgsxz4er68iCID+rsCAQM=",
"RemoteServerListUrl": "https://s3.amazonaws.com//psiphon/web/mjr4-p23r-puwl/server_list_compressed",
"SponsorId": "FFFFFFFFFFFFFFFF",
"UseIndistinguishableTLS": true
}
EOF
## removed parts:
# ,
# "EstablishTunnelTimeoutSeconds": 360,
# "TunnelPoolSize": 1
#
# also for WARP test:
# "UpstreamProxyURL": "socks5://127.0.0.1:40000",
#
chown "$PSIPHON_USER:$PSIPHON_GROUP" "$PSIPHON_CONFIG_FILE"
chmod 600 "$PSIPHON_CONFIG_FILE"
}
# Systemd service
function create_systemd_services() {
log "Creating systemd service..."
local service_script="$INSTALL_DIR/psiphon-tun-service.sh"
# Create service wrapper script
tee "$service_script" >/dev/null <<EOF
#!/bin/bash
set -euo pipefail
INSTALL_DIR="$INSTALL_DIR"
SERVICE_SCRIPT="$INSTALL_DIR/psiphon-tun.sh"
case "\${1:-}" in
start)
"\$SERVICE_SCRIPT" systemd_start
;;
stop)
"\$SERVICE_SCRIPT" systemd_stop
;;
reload)
"\$SERVICE_SCRIPT" systemd_reload
;;
restart)
"\$SERVICE_SCRIPT" systemd_restart
;;
*)
echo "Usage: \$0 {start|stop|restart|reload}"
exit 1
;;
esac
EOF
chmod 755 "$service_script"
chown root:root "$service_script"
# Create main systemd service file
tee /etc/systemd/system/$SERVICE_CONFIGURE_NAME.service >/dev/null <<EOF
[Unit]
Description=Psiphon TUN Service (Network Configuration)
After=network-online.target
Wants=network-online.target
Before=$SERVICE_BINARY_NAME.service
Documentation=https://github.com/boilingoden/psiphon-client-linux-service
[Service]
Type=simple
RemainAfterExit=yes
ExecStart=$service_script start
ExecStop=$service_script stop
ExecReload=$service_script reload
# TimeoutStartSec=120
# TimeoutStopSec=30
User=root
StandardOutput=journal
StandardError=journal
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=$INSTALL_DIR /run /var/log /etc/resolv.conf /etc/systemd/resolved.conf.d
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID CAP_SETGID CAP_AUDIT_WRITE CAP_IPC_LOCK
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_SETUID CAP_SETGID CAP_AUDIT_WRITE CAP_IPC_LOCK
SecureBits=keep-caps-locked
[Install]
WantedBy=multi-user.target
EOF
# Create tunnel service file
tee /etc/systemd/system/$SERVICE_BINARY_NAME.service >/dev/null <<EOF
[Unit]
Description=Psiphon Binary Process
After=network-online.target $SERVICE_CONFIGURE_NAME.service
Requires=$SERVICE_CONFIGURE_NAME.service
Documentation=https://github.com/boilingoden/psiphon-client-linux-service
StartLimitIntervalSec=10
StartLimitBurst=3
[Service]
Type=exec
# ExecStartPre=/bin/sleep 2
ExecStart="$PSIPHON_BINARY" -config "$PSIPHON_CONFIG_FILE" -dataRootDirectory "$INSTALL_DIR/data" \\
-tunDevice "$TUN_INTERFACE" -tunBindInterface "$TUN_BYPASS_INTERFACE" \\
-tunDNSServers "$TUN_DNS_SERVERS,$TUN_DNS_SERVERS6" -formatNotices -useNoticeFiles
# ExecStop=/bin/kill -TERM \$MAINPID
# ExecReload=/bin/systemctl --no-block restart %n
User=$PSIPHON_USER
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$SERVICE_BINARY_NAME
Restart=always
RestartSec=7s
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ReadWritePaths=$INSTALL_DIR /var/log
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
SecureBits=noroot-locked
ProtectClock=no
ProtectControlGroups=no
[Install]
WantedBy=multi-user.target
EOF
# Create homepage monitor service
tee /etc/systemd/system/$SERVICE_HOMEPAGE_MONITOR.path >/dev/null <<EOF
[Unit]
Description=Psiphon Homepage Monitor
[Path]
PathModified=$PSIPHON_SPONSOR_HOMEPAGE_PATH
Unit=$SERVICE_HOMEPAGE_TRIGGER.service
[Install]
WantedBy=multi-user.target
EOF
# Get the active logged-in user
# This checks for the active display manager session.
ACTIVE_USER=$(logname)
ACTIVE_USER_ID=$(id -u "$ACTIVE_USER" 2>/dev/null || echo "1000")
# Create the trigger service
tee /etc/systemd/system/$SERVICE_HOMEPAGE_TRIGGER.service >/dev/null <<EOF
[Unit]
Description=Psiphon Homepage Change Handler
# The service should only run after a graphical session has started.
PartOf=graphical.target
Requires=graphical.target
[Service]
Type=oneshot
Environment="DISPLAY=:0"
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$ACTIVE_USER_ID/bus"
ExecStart=/bin/sh -c 'journalctl -u $SERVICE_BINARY_NAME -n 5 --no-pager 2>/dev/null | grep -q "Tunnels.*count.*1" && notify-send -a "$SERVICE_CONFIGURE_NAME" -u critical -i network-vpn -t 10000 "Psiphon Connected" || notify-send -a "$SERVICE_CONFIGURE_NAME" -u normal -i network-vpn-disconnected -t 10000 "Psiphon Status Changed" "run: systemctl status $SERVICE_BINARY_NAME to check connection status"'
User=$ACTIVE_USER
# TODO: Make this open the URL in the user's default browser **securely**
# ExecStart=/bin/sh -c 'URL=\$(runuser -pu "$PSIPHON_USER" -- jq -r ".data.url" "$PSIPHON_SPONSOR_HOMEPAGE_PATH");echo "$\URL"; runuser -pu "$ACTIVE_USER" -- systemd-run --user xdg-open "\$URL" 2>/dev/null &'
# User=root
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
# ProtectHome=true
EOF
# Copy this script to install directory
cp -f "$0" "$INSTALL_DIR/psiphon-tun.sh"
chmod 755 "$INSTALL_DIR/psiphon-tun.sh"
chown root:root "$INSTALL_DIR/psiphon-tun.sh"
systemctl daemon-reload
success "Systemd service created"
}
# Change DNS Configuration
function change_dns_config() {
log "Setting up DNS configuration..."
# Backup original resolv.conf
if [ ! -f /etc/resolv.conf.original ]; then
cp -P /etc/resolv.conf /etc/resolv.conf.original &
wait
fi
# Check if systemd-resolved is running
if ! systemctl is-active systemd-resolved >/dev/null 2>&1; then
# Configure DNS servers
cat > /etc/resolv.conf <<EOF
nameserver 8.8.8.8
nameserver 8.8.4.4
nameserver 2001:4860:4860::8888
nameserver 2001:4860:4860::8844
EOF
# Set proper permissions
chmod 644 /etc/resolv.conf
# # Setup routing table for DNS
# for dns in 8.8.8.8 8.8.4.4; do
# ip route add $dns via $(ip route | grep default | grep -v $TUN_INTERFACE | awk '{print $3}') dev $TUN_BYPASS_INTERFACE proto static
# done
else
# Create resolved.conf drop-in directory if it doesn't exist
mkdir -p /etc/systemd/resolved.conf.d/
# Create custom configuration for DNS if it doesn't already exist
if [ ! -f /etc/systemd/resolved.conf.d/psiphon-tun.conf ]; then
# Create custom configuration for DNS
cat > /etc/systemd/resolved.conf.d/psiphon-tun.conf <<EOF
[Resolve]
DNS=8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844
DNSOverTLS=no
DNSSEC=no
Domains=~.
EOF
fi
# Set DNS routing for the TUN interface
resolvectl dns "$TUN_INTERFACE" 8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844
resolvectl domain "$TUN_INTERFACE" "~."
resolvectl default-route "$TUN_INTERFACE" yes
# Restart systemd-resolved to apply changes
systemctl restart systemd-resolved
fi
success "TUN interface configured with IPv4 and IPv6"
}
# Setup TUN interface
function setup_tun_interface() {
log "Setting up TUN interface..."
# # DYNAMIC INTERFACE DETECTION: Re-determine bypass interface if not set or in service mode
# # In systemd service mode, the network may not have been ready when the script loaded,
# # so we need to re-detect the bypass interface now
# if [[ -z "$TUN_BYPASS_INTERFACE" ]] || [[ "$SERVICE_MODE" == "true" ]]; then
# log "Re-detecting bypass interface for current network state..."
# local detected_interface
# detected_interface=$(ip -json route get 8.8.8.8 2>/dev/null | jq -r '.[0].dev // empty' ||
# ip -json link show | jq -r '.[] | select(.link_type!="loopback") | .ifname' | head -n1)
# if [[ -n "$detected_interface" ]]; then
# if [[ "$TUN_BYPASS_INTERFACE" != "$detected_interface" ]]; then
# log "Bypass interface changed from '$TUN_BYPASS_INTERFACE' to '$detected_interface'"
# fi
# TUN_BYPASS_INTERFACE="$detected_interface"
# elif [[ -z "$TUN_BYPASS_INTERFACE" ]]; then
# error "Could not determine bypass interface - network may not be ready"
# error "Initial detection also failed. Check network connectivity."
# return 1
# else
# log "Using previously detected bypass interface: $TUN_BYPASS_INTERFACE"
# fi
# fi
# log "Using bypass interface: $TUN_BYPASS_INTERFACE"
# Create TUN interface if it doesn't exist
if ! ip link show "$TUN_INTERFACE" >/dev/null 2>&1; then
if ! ip tuntap add dev "$TUN_INTERFACE" mode tun user "$PSIPHON_USER" group "$PSIPHON_GROUP"; then
error "Failed to create TUN interface"
cleanup_routing
return 1
fi
fi
# Configure IPv4 interface
if ! ip addr flush dev "$TUN_INTERFACE" 2>&1; then
warning "Failed to flush TUN interface: $?"
fi
# Configure IPv4 point-to-point addresses
if ! ip addr add "$TUN_IP" peer "$TUN_PEER_IP" dev "$TUN_INTERFACE" 2>&1; then
error "Failed to add IPv4 point-to-point address to TUN interface"
cleanup_routing
return 1
fi
# Configure IPv6 interface with unique local address
if ! ip -6 addr add "$TUN_IP6/64" dev "$TUN_INTERFACE" 2>&1; then
warning "Failed to add IPv6 address to TUN interface: $?"
fi
# Bring up interface and wait for it to be ready
ip link set "$TUN_INTERFACE" up
# Wait for interface to be ready (both IPv4 and IPv6)
local timeout=10
while [ "$timeout" -gt 1 ]; do
if ip addr show dev "$TUN_INTERFACE" | grep -q "inet.*$TUN_IP" && \
ip addr show dev "$TUN_INTERFACE" | grep -q "inet6.*$TUN_IP6"; then
break
fi
sleep 1
((timeout--))
done
if [ "$timeout" -eq 0 ]; then
warning "Timeout waiting for TUN interface to be fully ready"
else
log "TUN interface ready with both IPv4 and IPv6 addresses"
fi
# Verify psiphon-user can access TUN device before proceeding
# This prevents silent failures when Psiphon later tries to bind to the TUN interface
log "Verifying psiphon-user access to TUN device..."
if ! sudo -u "$PSIPHON_USER" test -r "/dev/net/tun" 2>/dev/null || \
! sudo -u "$PSIPHON_USER" test -w "/dev/net/tun" 2>/dev/null; then
error "User $PSIPHON_USER cannot read/write /dev/net/tun device"
error "Check TUN interface permissions and group membership"
cleanup_routing
return 1
fi
log "✓ TUN device access verified for psiphon-user"
# DON'T add default routes here - wait for RA processing
log "TUN interface configured (routes will be added after RA processing)"
# Update DNS configuration after routes are established
change_dns_config
# Verify DNS system is ready before proceeding
# systemd-resolved may need time to reload configuration
log "Waiting for DNS system to stabilize..."
local dns_wait=0
local dns_max_wait=10
while [ $dns_wait -lt $dns_max_wait ]; do
if systemctl is-active --quiet systemd-resolved 2>/dev/null; then
log "✓ systemd-resolved is active"
break
fi
sleep 1
dns_wait=$((dns_wait + 1))
done
if [ $dns_wait -eq $dns_max_wait ]; then
warning "systemd-resolved not confirmed active after ${dns_max_wait}s, but proceeding"
fi
success "TUN interface configured with IPv4 and IPv6 addresses"
}
# Network Security and Kill Switch Implementation
# Configures comprehensive traffic isolation and routing enforcement using nftables
# Configure nftables firewall rules for secure VPN operation
configure_nftables() {
local tun_interface="$TUN_INTERFACE"
local tun_subnet="$TUN_SUBNET"
local tun_subnet6="$TUN_SUBNET6"
local bypass_interface="$TUN_BYPASS_INTERFACE"
# Get the numeric UID for psiphon-user (required for nftables meta skuid)
local psiphon_uid
psiphon_uid=$(id -u "$PSIPHON_USER" 2>/dev/null)
if [ -z "$psiphon_uid" ]; then
error "Failed to get UID for $PSIPHON_USER"
return 1
fi
# Check if nftables is available and working
if ! command -v nft &>/dev/null; then
error "nftables (nft command) is not available. Please install the nftables package."
return 1
fi
# Verify nftables can be used (test with a simple list command)
local nft_test
if ! nft_test=$(nft list tables 2>&1 >/dev/null); then
error "nftables command failed. This may indicate:"
error " 1. nftables service is not running (try: systemctl start nftables)"
error " 2. Missing kernel support for nftables"
error " 3. Permission issues (this should be run as root)"
error " Raw error: $nft_test"
return 1
fi
log "Configuring nftables with UID $psiphon_uid for $PSIPHON_USER"
# Create a temporary file to build the nftables ruleset
# This ensures safe configuration before applying
local nft_ruleset_file
nft_ruleset_file=$(mktemp) || {
error "Failed to create temporary file for nftables configuration"
return 1
}
# shellcheck disable=SC2064
# Double quotes intentionally used here to capture the local variable value
# while the function is still executing. Single quotes would cause the
# variable to be undefined when the trap fires (out of scope).
trap "rm -f '$nft_ruleset_file'" RETURN
# Generate the nftables ruleset with proper variable expansion
cat > "$nft_ruleset_file" << EOF
# Remove and recreate only Psiphon-specific tables (idempotent, non-destructive)
flush ruleset
# Define filter table (inet covers both IPv4 and IPv6)
table inet psiphon_filter {
chain input {
type filter hook input priority 0; policy accept;
}
chain forward {
type filter hook forward priority 0; policy drop;
# Allow established/related connections first
ct state {established, related} accept
# Allow traffic through TUN interface (both directions)
iifname "$tun_interface" accept
oifname "$tun_interface" accept
}
chain output {
type filter hook output priority 0; policy drop;
# Allow loopback traffic (both directions for local services)
iifname "lo" accept
oifname "lo" accept
# Allow established/related connections first
ct state {established, related} accept
# Allow Psiphon user (UID $psiphon_uid) to use any interface (needed for tunnel establishment)
# This must come before TUN-only rule to allow tunnel bootstrap
meta skuid $psiphon_uid accept
# Allow TUN interface traffic for everyone else
oifname "$tun_interface" accept
# Everyone else can ONLY use TUN interface (default DROP policy handles blocking)
}
}
# IPv4 NAT table
table ip psiphon_nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# NAT for IPv4 traffic from TUN subnet going out through bypass interface
ip saddr $tun_subnet oifname "$bypass_interface" masquerade
}
}
# IPv6 NAT table
table ip6 psiphon_nat6 {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
# NAT for IPv6 traffic from TUN subnet going out through bypass interface
ip6 saddr $tun_subnet6 oifname "$bypass_interface" masquerade
}
}
EOF
# Validate the ruleset before applying (show errors for debugging)
local validation_output
if ! validation_output=$(nft -c -f "$nft_ruleset_file" 2>&1); then
error "nftables ruleset validation failed. Generated ruleset is invalid."
error "Validation error details:"
echo "$validation_output" | while read -r line; do
error " $line"
done
# Also log the generated ruleset for debugging
error "Generated ruleset that failed validation:"
while read -r line; do
error " $line"
done < "$nft_ruleset_file"
return 1
fi
log "nftables ruleset validation passed"
# Apply the validated ruleset
local apply_error
local apply_status
apply_error=$(nft -f "$nft_ruleset_file" 2>&1)
apply_status=$?
if [ $apply_status -ne 0 ]; then
error "Failed to apply nftables rules (exit code: $apply_status)"
error "Application error details:"
echo "$apply_error" | while read -r line; do
error " $line"
done
error "Generated ruleset that failed application:"
while read -r line; do
error " $line"
done < "$nft_ruleset_file"
return 1
fi
if [ -n "$apply_error" ]; then
log "nftables applied with warnings/output: $apply_error"
fi
# Verify the rules were actually applied
log "Verifying nftables rules were applied successfully..."
# First check if nft command exists
if ! command -v nft >/dev/null 2>&1; then
error "nft command not found - nftables may not be installed"
return 1
fi
# Check if the table exists
local table_check
if ! table_check=$(nft list tables inet 2>&1); then
error "Failed to list nftables tables"
error " Error: $table_check"
return 1
fi
if ! nft list chain inet psiphon_filter output >/dev/null 2>&1; then
error "nftables rules were not properly applied - OUTPUT chain not found"
error "Available tables:"
nft list tables 2>&1 | while read -r line; do
error " $line"
done
error "Attempting to list all rules for debugging:"
nft list ruleset 2>&1 | while read -r line; do
error " $line"
done
return 1
fi
log "✓ nftables rules successfully applied and verified"
# Save nftables rules for persistence
log "Saving nftables configuration for persistence..."
# Create /etc/nftables directory if it doesn't exist
mkdir -p "/etc/nftables" 2>/dev/null || true
# Save the validated ruleset for persistence
# Use the temporary file we already validated
if ! cp "$nft_ruleset_file" "/etc/nftables/psiphon-tun.nft" 2>/dev/null; then
warning "Failed to save nftables ruleset to /etc/nftables/psiphon-tun.nft (persistence may not work across reboots)"
else
log "nftables ruleset saved to /etc/nftables/psiphon-tun.nft"
fi
# Ensure the main config file includes our ruleset
if [ -f "/etc/nftables.conf" ]; then
# Remove any old psiphon includes first to avoid duplicates
sed -i '/psiphon-tun/d' "/etc/nftables.conf" 2>/dev/null || true
# Add our include if not present
if ! grep -q 'psiphon-tun.nft' "/etc/nftables.conf" 2>/dev/null; then
if echo 'include "/etc/nftables/psiphon-tun.nft"' >> "/etc/nftables.conf" 2>/dev/null; then
log "Added psiphon-tun.nft include to /etc/nftables.conf"
fi
fi
else
# If main config doesn't exist, create a new one with our rules
if cat > "/etc/nftables.conf" << 'NFTCONF' 2>/dev/null
#!/usr/sbin/nft -f
flush ruleset
include "/etc/nftables/psiphon-tun.nft"
NFTCONF
then
log "Created /etc/nftables.conf with psiphon-tun.nft include"
else
warning "Failed to create /etc/nftables.conf (system may not auto-load rules on reboot)"
fi
fi
# Ensure nftables service is enabled and running
# Ensure nftables service is enabled and running
if ! systemctl is-enabled nftables &>/dev/null 2>&1; then
if systemctl enable nftables 2>/dev/null; then