name: Ubuntu 24.04 acceptance
on:
workflow_dispatch:
push:
branches: [main]
paths:
- .github/workflows/ubuntu-acceptance.yml
- Cargo.toml
- Cargo.lock
- src/**
- scripts/install.sh
permissions:
contents: read
env:
PUBLIC_VERSION: 0.1.6
jobs:
root-systemd:
name: Public install and root/systemd runtime
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
- uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32
- name: Install PTY test dependency
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends expect
- name: Verify clean Ubuntu 24.04 x86_64 baseline
shell: bash
run: |
set -euo pipefail
. /etc/os-release
test "$ID" = ubuntu
test "$VERSION_ID" = 24.04
test "$(uname -m)" = x86_64
systemctl --version
test ! -e /usr/local/bin/shoes
test ! -e /etc/shoes
test ! -e /etc/systemd/system/shoes.service
- name: Verify public crates.io installation
shell: bash
run: |
set -euo pipefail
public_root="$RUNNER_TEMP/ping-rust-public"
cargo install ping-rust \
--version "$PUBLIC_VERSION" \
--locked \
--root "$public_root"
test "$("$public_root/bin/ping-rust" --version)" = \
"ping-rust $PUBLIC_VERSION"
- name: Install current source under test
shell: bash
run: |
set -euo pipefail
current_root="$RUNNER_TEMP/ping-rust-current"
cargo install --path . --locked --root "$current_root"
ping_rust="$current_root/bin/ping-rust"
ln -s ping-rust "$current_root/bin/prs"
"$ping_rust" --version
printf 'PING_RUST=%s\n' "$ping_rust" >> "$GITHUB_ENV"
printf 'PRS=%s\n' "$current_root/bin/prs" >> "$GITHUB_ENV"
- name: Installer bootstrap deploys random-port Reality without input
id: reality
shell: bash
run: |
set -euo pipefail
started_at="$(date +%s)"
bootstrap_output="$RUNNER_TEMP/reality-bootstrap.txt"
sudo "$PING_RUST" bootstrap | sudo tee "$bootstrap_output" >/dev/null \
|| { echo 'bootstrap command failed' >&2; exit 1; }
! sudo grep -Eq '选择协议|输入端口' "$bootstrap_output" \
|| { echo 'bootstrap prompted unexpectedly' >&2; exit 1; }
sudo grep -q '首次安装:自动部署 VLESS-REALITY' "$bootstrap_output" \
|| { echo 'bootstrap marker missing' >&2; exit 1; }
sudo grep -q 'vless://' "$bootstrap_output" \
|| { echo 'bootstrap URI missing' >&2; exit 1; }
reality_uri="$(sudo "$PING_RUST" url)"
sudo rm -f "$bootstrap_output"
printf '%s' "$reality_uri" | grep -q '^vless://' \
|| { echo 'saved URI missing' >&2; exit 1; }
for parameter in security=reality flow=xtls-rprx-vision pbk= sid=; do
printf '%s' "$reality_uri" | grep -q "$parameter" \
|| { echo 'saved URI parameter missing' >&2; exit 1; }
done
read -r reality_sni reality_fingerprint < <(
REALITY_URI="$reality_uri" python3 - <<'PY'
import os
from urllib.parse import parse_qs, urlparse
query = parse_qs(urlparse(os.environ['REALITY_URI']).query)
print(query.get('sni', [''])[0], query.get('fp', [''])[0])
PY
)
case "$reality_sni" in
www.amazon.com|www.ebay.com|www.paypal.com|www.cloudflare.com|dash.cloudflare.com|aws.amazon.com) ;;
*) echo "unexpected Reality SNI: $reality_sni" >&2; exit 1 ;;
esac
test "$reality_fingerprint" = chrome \
|| { echo "unexpected Reality fingerprint: $reality_fingerprint" >&2; exit 1; }
test "$(sudo "$PING_RUST" qr 2>/dev/null)" = "$reality_uri" \
|| { echo 'QR fallback URI mismatch' >&2; exit 1; }
info_output="$(sudo "$PING_RUST" info)"
printf '%s' "$info_output" | grep -q '客户端地址:' \
|| { echo 'client address missing from info' >&2; exit 1; }
printf '%s' "$info_output" | grep -Fq "| $reality_sni" \
|| { echo 'Reality SNI differs between info summary and URI' >&2; exit 1; }
sudo grep -Fq "$reality_sni:" /etc/shoes/config.yaml \
|| { echo 'Reality SNI missing from shoes YAML' >&2; exit 1; }
sudo grep -Fq "\"server_name\": \"$reality_sni\"" \
/etc/shoes/ping-rust-state.json \
|| { echo 'Reality SNI missing from managed state' >&2; exit 1; }
reality_port="$(
printf '%s' "$info_output" \
| sed -nE 's/.*0\.0\.0\.0:([0-9]+).*/\1/p' \
| head -n 1
)"
test -n "$reality_port" \
|| { echo 'Reality port missing from info' >&2; exit 1; }
test "$reality_port" -ge 20000 \
|| { echo 'Reality port outside random range' >&2; exit 1; }
printf 'REALITY_PORT=%s\n' "$reality_port" >> "$GITHUB_ENV"
elapsed="$(( $(date +%s) - started_at ))"
test "$elapsed" -le 180 \
|| { echo 'Reality bootstrap exceeded three minutes' >&2; exit 1; }
printf 'elapsed=%s\n' "$elapsed" >> "$GITHUB_OUTPUT"
sudo systemctl is-active --quiet shoes.service \
|| { echo 'shoes service is not active' >&2; exit 1; }
sudo systemctl is-enabled --quiet shoes.service \
|| { echo 'shoes service is not enabled' >&2; exit 1; }
for _ in {1..20}; do
if sudo ss -H -lnt | awk '{print $4}' | grep -Eq "(^|:)${reality_port}$"; then
break
fi
sleep 0.5
done
sudo ss -H -lnt | awk '{print $4}' | grep -Eq "(^|:)${reality_port}$" \
|| { echo 'Reality listener missing' >&2; exit 1; }
sudo test -s "/etc/shoes/profiles/VLESS-REALITY-${reality_port}.yaml" \
|| { echo 'Reality profile file missing' >&2; exit 1; }
reality_profile="$(
printf '%s' "$info_output" \
| grep -Eo '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' \
| head -n 1
)"
test -n "$reality_profile" \
|| { echo 'Reality profile UUID missing' >&2; exit 1; }
printf 'REALITY_PROFILE=%s\n' "$reality_profile" >> "$GITHUB_ENV"
- name: View the only profile without a selection menu
shell: bash
run: |
set -euo pipefail
sudo env PRS_COMMAND="$PRS" expect <<'EXPECT'
log_user 0
set timeout 180
spawn $env(PRS_COMMAND)
expect -re {请选择 \[0-10\]}
send "3\r"
expect {
-re {请选择配置} {
puts stderr "single profile unexpectedly showed a selection menu"
exit 41
}
-re {协议 \(protocol\)[^\r\n]*= vless} {}
eof {
puts stderr "view exited before rendering the only profile"
exit 42
}
timeout {
puts stderr "timeout waiting for the only profile details"
exit 43
}
}
expect {
-re {vless://} {}
eof {
puts stderr "view exited before rendering the share URI"
exit 44
}
timeout {
puts stderr "timeout waiting for the only profile URI"
exit 45
}
}
expect {
eof {}
timeout {
puts stderr "single-profile view did not exit"
exit 46
}
}
EXPECT
- name: Add Hysteria2 and confirm service reload
shell: bash
run: |
set -euo pipefail
previous_pid="$(sudo systemctl show --property MainPID --value shoes.service)"
test "$previous_pid" -gt 0
hysteria_output="$RUNNER_TEMP/hysteria2-generation.txt"
sudo "$PING_RUST" generate hysteria2 \
--name acceptance-hysteria2 \
--port 24444 \
--server-name localhost \
| sudo tee "$hysteria_output" >/dev/null
current_pid="$(sudo systemctl show --property MainPID --value shoes.service)"
test "$current_pid" -gt 0
test "$current_pid" != "$previous_pid"
sudo systemctl is-active --quiet shoes.service
for _ in {1..20}; do
if sudo ss -H -lun | awk '{print $4}' | grep -Eq '(^|:)24444$'; then
break
fi
sleep 0.5
done
sudo ss -H -lun | awk '{print $4}' | grep -Eq '(^|:)24444$'
hysteria_profile="$(
grep -Eo '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' \
"$hysteria_output" | head -n 1
)"
test -n "$hysteria_profile"
printf 'HYSTERIA_PROFILE=%s\n' "$hysteria_profile" >> "$GITHUB_ENV"
sudo rm -f "$hysteria_output"
- name: Add TUIC and confirm service reload
shell: bash
run: |
set -euo pipefail
previous_pid="$(sudo systemctl show --property MainPID --value shoes.service)"
test "$previous_pid" -gt 0
tuic_output="$RUNNER_TEMP/tuic-generation.txt"
sudo "$PING_RUST" generate tuic \
--name acceptance-tuic \
--port 24445 \
--server-name localhost \
| sudo tee "$tuic_output" >/dev/null
current_pid="$(sudo systemctl show --property MainPID --value shoes.service)"
test "$current_pid" -gt 0
test "$current_pid" != "$previous_pid"
sudo systemctl is-active --quiet shoes.service
for _ in {1..20}; do
if sudo ss -H -lun | awk '{print $4}' | grep -Eq '(^|:)24445$'; then
break
fi
sleep 0.5
done
sudo ss -H -lun | awk '{print $4}' | grep -Eq '(^|:)24445$'
tuic_profile="$(
grep -Eo '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' \
"$tuic_output" | head -n 1
)"
test -n "$tuic_profile"
printf 'TUIC_PROFILE=%s\n' "$tuic_profile" >> "$GITHUB_ENV"
sudo rm -f "$tuic_output"
- name: Add Shadowsocks and AnyTLS and confirm core protocol runtime
shell: bash
run: |
set -euo pipefail
shadowsocks_output="$RUNNER_TEMP/shadowsocks-generation.txt"
sudo "$PING_RUST" generate shadowsocks \
--name acceptance-shadowsocks \
--port 24446 \
| sudo tee "$shadowsocks_output" >/dev/null
for _ in {1..20}; do
if sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)24446$'; then
break
fi
sleep 0.5
done
sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)24446$'
shadowsocks_profile="$(
grep -Eo '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' \
"$shadowsocks_output" | head -n 1
)"
test -n "$shadowsocks_profile"
printf 'SHADOWSOCKS_PROFILE=%s\n' "$shadowsocks_profile" >> "$GITHUB_ENV"
sudo rm -f "$shadowsocks_output"
anytls_output="$RUNNER_TEMP/anytls-generation.txt"
sudo "$PING_RUST" generate anytls \
--name acceptance-anytls \
--port 24447 \
--server-name localhost \
--user acceptance:anytls-acceptance-password \
--padding stop=8 \
--padding 0=30-30 \
| sudo tee "$anytls_output" >/dev/null
sudo systemctl is-active --quiet shoes.service
for _ in {1..20}; do
if sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)24447$'; then
break
fi
sleep 0.5
done
sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)24447$'
anytls_profile="$(
grep -Eo '[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}' \
"$anytls_output" | head -n 1
)"
test -n "$anytls_profile"
printf 'ANYTLS_PROFILE=%s\n' "$anytls_profile" >> "$GITHUB_ENV"
sudo rm -f "$anytls_output"
for profile_file in \
"VLESS-REALITY-${REALITY_PORT}.yaml" \
HYSTERIA2-24444.yaml \
TUIC-24445.yaml \
SHADOWSOCKS-24446.yaml \
ANYTLS-24447.yaml; do
sudo test -s "/etc/shoes/profiles/$profile_file"
done
- name: Verify prs numeric PTY flow for all protocol presets
shell: bash
run: |
set -euo pipefail
config_before_migration="$(sudo sha256sum /etc/shoes/config.yaml | awk '{print $1}')"
state_before_migration="$(sudo sha256sum /etc/shoes/ping-rust-state.json | awk '{print $1}')"
sudo mv /etc/shoes/profiles "$RUNNER_TEMP/legacy-profiles"
sudo env PRS_COMMAND="$PRS" expect <<'EXPECT'
log_user 0
set timeout 180
spawn $env(PRS_COMMAND)
expect -re {请选择 \[0-10\]}
send "0\r"
expect eof
EXPECT
test "$(sudo sha256sum /etc/shoes/config.yaml | awk '{print $1}')" = \
"$config_before_migration"
test "$(sudo sha256sum /etc/shoes/ping-rust-state.json | awk '{print $1}')" = \
"$state_before_migration"
for profile_file in \
"VLESS-REALITY-${REALITY_PORT}.yaml" \
HYSTERIA2-24444.yaml \
TUIC-24445.yaml \
SHADOWSOCKS-24446.yaml \
ANYTLS-24447.yaml; do
sudo test -s "/etc/shoes/profiles/$profile_file"
done
sudo rm -r "$RUNNER_TEMP/legacy-profiles"
run_menu_add() {
local protocol_number="$1"
local port="$2"
local scheme="$3"
sudo env PRS_COMMAND="$PRS" PROTOCOL_NUMBER="$protocol_number" \
MENU_PORT="$port" SHARE_SCHEME="$scheme" expect <<'EXPECT'
log_user 0
set timeout 180
proc wait_for {pattern stage} {
expect {
-re $pattern { return }
eof { puts stderr "unexpected EOF at $stage"; exit 21 }
timeout { puts stderr "timeout at $stage"; exit 22 }
}
}
spawn $env(PRS_COMMAND)
wait_for {请选择 \[0-10\]} main-menu
send "1\r"
wait_for {请选择 \[0-10\]} protocol-menu
send "$env(PROTOCOL_NUMBER)\r"
wait_for {输入端口} port-prompt
send "$env(MENU_PORT)\r"
if {$env(PROTOCOL_NUMBER) == "3"} {
wait_for {请选择加密方式} shadowsocks-cipher-menu
send "\r"
wait_for {请设置密码} shadowsocks-password-prompt
send "\r"
}
wait_for "$env(SHARE_SCHEME)://" share-uri
expect { eof {} timeout { puts stderr "timeout waiting for add exit"; exit 23 } }
EXPECT
}
run_menu_add 4 25448 vless
run_menu_add 3 25449 ss
run_menu_add 6 25451 vless
run_menu_add 7 25452 vless
run_menu_add 8 25453 trojan
run_menu_add 9 25454 trojan
run_menu_add 10 25455 vmess
sudo systemctl is-active --quiet shoes.service
wait_listener() {
local kind="$1"
local port="$2"
for _ in {1..20}; do
if sudo ss -H "-ln${kind}" | awk '{print $4}' | grep -Eq "(^|:)${port}$"; then
return 0
fi
sleep 0.5
done
echo "missing ${kind} listener on ${port}" >&2
return 1
}
wait_listener t 25448
wait_listener t 25449
wait_listener t 25451
wait_listener t 25452
wait_listener t 25453
wait_listener t 25454
wait_listener t 25455
profile_id_for_port() {
local port="$1"
sudo python3 - "$port" <<'PY'
import json
import sys
with open('/etc/shoes/ping-rust-state.json', encoding='utf-8') as stream:
profiles = json.load(stream)['profiles']
print(next(profile['id'] for profile in profiles if profile['port'] == int(sys.argv[1])))
PY
}
printf 'VLESS_TLS_PROFILE=%s\n' "$(profile_id_for_port 25451)" >> "$GITHUB_ENV"
printf 'VLESS_WS_TLS_PROFILE=%s\n' "$(profile_id_for_port 25452)" >> "$GITHUB_ENV"
printf 'TROJAN_TLS_PROFILE=%s\n' "$(profile_id_for_port 25453)" >> "$GITHUB_ENV"
printf 'TROJAN_REALITY_PROFILE=%s\n' "$(profile_id_for_port 25454)" >> "$GITHUB_ENV"
printf 'VMESS_WS_TLS_PROFILE=%s\n' "$(profile_id_for_port 25455)" >> "$GITHUB_ENV"
for profile_file in \
VLESS-TLS-VISION-25451.yaml \
VLESS-WS-TLS-25452.yaml \
TROJAN-TLS-25453.yaml \
TROJAN-REALITY-25454.yaml \
VMESS-WS-TLS-25455.yaml; do
sudo test -s "/etc/shoes/profiles/$profile_file"
done
profile_number="$(sudo python3 - <<'PY'
import json
with open('/etc/shoes/ping-rust-state.json', encoding='utf-8') as stream:
profiles = json.load(stream)['profiles']
print(next(index for index, profile in enumerate(profiles, 1) if profile['port'] == 25448))
PY
)"
sudo env PRS_COMMAND="$PRS" PROFILE_NUMBER="$profile_number" expect <<'EXPECT'
log_user 0
set timeout 180
proc wait_for {pattern stage} {
expect {
-re $pattern { return }
eof { puts stderr "unexpected EOF at $stage"; exit 31 }
timeout { puts stderr "timeout at $stage"; exit 32 }
}
}
spawn $env(PRS_COMMAND)
wait_for {请选择 \[0-10\]} main-menu
send "2\r"
wait_for {请选择配置} profile-menu
wait_for {请选择} profile-prompt
send "$env(PROFILE_NUMBER)\r"
wait_for {选择更改项目} change-menu
wait_for {请选择 \[0-5\]} change-prompt
send "1\r"
wait_for {输入新端口} port-prompt
send "25450\r"
wait_for {配置更改成功} change-success
wait_for {请选择 \[0-10\]} main-menu-return
send "0\r"
wait_for {已退出} exit-message
expect { eof {} timeout { puts stderr "timeout waiting for change exit"; exit 33 } }
EXPECT
wait_listener t 25450
! sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)25448$'
sudo test ! -e /etc/shoes/profiles/VLESS-REALITY-25448.yaml
sudo test -s /etc/shoes/profiles/VLESS-REALITY-25450.yaml
sudo test -s /etc/shoes/profiles/SHADOWSOCKS-25449.yaml
sudo grep -q '^address: 0.0.0.0:25450$' \
/etc/shoes/profiles/VLESS-REALITY-25450.yaml
ss_profile_number="$(sudo python3 - <<'PY'
import json
with open('/etc/shoes/ping-rust-state.json', encoding='utf-8') as stream:
profiles = json.load(stream)['profiles']
print(next(index for index, profile in enumerate(profiles, 1) if profile['port'] == 25449))
PY
)"
sudo env PRS_COMMAND="$PRS" PROFILE_NUMBER="$profile_number" \
SS_PROFILE_NUMBER="$ss_profile_number" expect <<'EXPECT'
log_user 0
set timeout 180
proc wait_for {pattern stage} {
expect {
-re $pattern { return }
eof { puts stderr "unexpected EOF at $stage"; exit 34 }
timeout { puts stderr "timeout at $stage"; exit 35 }
}
}
spawn $env(PRS_COMMAND)
wait_for {请选择 \[0-10\]} main-menu
send "3\r"
wait_for {请选择配置} view-profile-menu
wait_for {VLESS-REALITY-25450\.yaml} view-profile-file
wait_for {请选择} view-profile-prompt
send "$env(PROFILE_NUMBER)\r"
wait_for {协议 \(protocol\)[^\r\n]*= vless} view-info
wait_for {vless://} view-uri
expect { eof {} timeout { puts stderr "timeout waiting for view exit"; exit 36 } }
spawn $env(PRS_COMMAND)
wait_for {请选择 \[0-10\]} main-menu-after-view
send "4\r"
wait_for {请选择配置} delete-profile-menu
wait_for {VLESS-REALITY-25450\.yaml} delete-profile-file
wait_for {请选择} delete-profile-prompt
send "0\r"
wait_for {请选择 \[0-10\]} main-menu-after-delete-return
send "4\r"
wait_for {请选择配置} delete-profile-menu-again
wait_for {请选择} delete-profile-prompt-again
send "$env(SS_PROFILE_NUMBER)\r"
wait_for {确认删除 SHADOWSOCKS-25449\.yaml} delete-confirm
send "y\r"
wait_for {已删除.*SHADOWSOCKS-25449\.yaml} delete-success
wait_for {请选择 \[0-10\]} main-menu-after-delete
send "0\r"
wait_for {已退出} exit-message
expect { eof {} timeout { puts stderr "timeout waiting for delete exit"; exit 37 } }
EXPECT
sudo test ! -e /etc/shoes/profiles/SHADOWSOCKS-25449.yaml
! sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)25449$'
- name: Roll back config and systemd state when activation fails
shell: bash
run: |
set -euo pipefail
config_before="$(sudo sha256sum /etc/shoes/config.yaml | awk '{print $1}')"
state_before="$(sudo sha256sum /etc/shoes/ping-rust-state.json | awk '{print $1}')"
profiles_before="$(sudo find /etc/shoes/profiles -maxdepth 1 -type f \
-print0 | sort -z | sudo xargs -0 -r sha256sum | sha256sum | awk '{print $1}')"
unit_before="$(sudo sha256sum /etc/systemd/system/shoes.service | awk '{print $1}')"
sudo systemctl is-active --quiet shoes.service
sudo systemctl is-enabled --quiet shoes.service
sudo tee /run/ping-rust-fail-once.sh >/dev/null <<'FAIL_ONCE'
#!/usr/bin/env bash
marker=/dev/shm/ping-rust-activation-fault
if [ ! -e "$marker" ]; then
touch "$marker"
exit 42
fi
exit 0
FAIL_ONCE
sudo chmod 0755 /run/ping-rust-fail-once.sh
sudo mkdir -p /etc/systemd/system/shoes.service.d
sudo tee /etc/systemd/system/shoes.service.d/activation-fault.conf >/dev/null <<'FAULT_DROPIN'
[Service]
ExecStartPre=/run/ping-rust-fail-once.sh
FAULT_DROPIN
sudo systemctl daemon-reload
rollback_port="$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')"
failure_log="$RUNNER_TEMP/activation-failure.txt"
set +e
sudo "$PING_RUST" add reality \
--name must-rollback \
--port "$rollback_port" \
--server-address 203.0.113.10 \
--plain >"$failure_log" 2>&1
add_status="$?"
set -e
sudo test -e /dev/shm/ping-rust-activation-fault \
|| { echo 'activation fault did not execute' >&2; exit 1; }
sudo rm -f /etc/systemd/system/shoes.service.d/activation-fault.conf \
/run/ping-rust-fail-once.sh /dev/shm/ping-rust-activation-fault
sudo rmdir /etc/systemd/system/shoes.service.d
sudo systemctl daemon-reload
test "$add_status" -ne 0 \
|| { echo 'activation failure injection unexpectedly succeeded' >&2; exit 1; }
grep -q '回滚' "$failure_log" || { echo 'missing rollback marker' >&2; exit 1; }
! grep -q 'vless://' "$failure_log" || { echo 'failure leaked share URI' >&2; exit 1; }
test "$(sudo sha256sum /etc/shoes/config.yaml | awk '{print $1}')" = "$config_before" \
|| { echo 'config hash mismatch' >&2; exit 1; }
test "$(sudo sha256sum /etc/shoes/ping-rust-state.json | awk '{print $1}')" = "$state_before" \
|| { echo 'state hash mismatch' >&2; exit 1; }
test "$(sudo find /etc/shoes/profiles -maxdepth 1 -type f \
-print0 | sort -z | sudo xargs -0 -r sha256sum | sha256sum | awk '{print $1}')" = "$profiles_before" \
|| { echo 'profile directory hash mismatch' >&2; exit 1; }
test "$(sudo sha256sum /etc/systemd/system/shoes.service | awk '{print $1}')" = "$unit_before" \
|| { echo 'unit hash mismatch' >&2; exit 1; }
sudo systemctl is-active --quiet shoes.service \
|| { echo 'service not active after rollback' >&2; exit 1; }
sudo systemctl is-enabled --quiet shoes.service \
|| { echo 'service not enabled after rollback' >&2; exit 1; }
! sudo ss -H -lnt | awk '{print $4}' | grep -Eq "(^|:)${rollback_port}$" \
|| { echo 'rolled-back port still listening' >&2; exit 1; }
rm -f "$failure_log"
- name: Verify client exports and operations
shell: bash
run: |
set -euo pipefail
for entry in \
"reality:$REALITY_PROFILE" \
"hysteria2:$HYSTERIA_PROFILE" \
"tuic:$TUIC_PROFILE" \
"shadowsocks:$SHADOWSOCKS_PROFILE" \
"anytls:$ANYTLS_PROFILE" \
"vless-tls:$VLESS_TLS_PROFILE" \
"vless-ws-tls:$VLESS_WS_TLS_PROFILE" \
"trojan-tls:$TROJAN_TLS_PROFILE" \
"trojan-reality:$TROJAN_REALITY_PROFILE" \
"vmess-ws-tls:$VMESS_WS_TLS_PROFILE"; do
name="${entry%%:*}"
profile="${entry#*:}"
sudo "$PING_RUST" export clash-meta \
--profile "$profile" \
--server 127.0.0.1 \
--output "$RUNNER_TEMP/$name-clash.yaml"
sudo "$PING_RUST" export sing-box \
--profile "$profile" \
--server 127.0.0.1 \
--output "$RUNNER_TEMP/$name-sing-box.json"
sudo "$PING_RUST" export nekobox \
--profile "$profile" \
--server 127.0.0.1 \
--output "$RUNNER_TEMP/$name-nekobox.txt"
sudo test -s "$RUNNER_TEMP/$name-clash.yaml"
sudo test -s "$RUNNER_TEMP/$name-sing-box.json"
sudo test -s "$RUNNER_TEMP/$name-nekobox.txt"
sudo python3 -m json.tool \
"$RUNNER_TEMP/$name-sing-box.json" >/dev/null
done
backup="$RUNNER_TEMP/shoes-backup.tar.gz"
sudo "$PING_RUST" backup "$backup"
sudo test -s "$backup"
sudo tar -tzf "$backup" | grep -q 'shoes/profiles/VLESS-REALITY-'
sudo tar -tzf "$backup" | grep -q 'shoes/profiles/HYSTERIA2-'
sudo "$PING_RUST" restore "$backup"
sudo test -s "/etc/shoes/profiles/VLESS-REALITY-${REALITY_PORT}.yaml"
sudo "$PING_RUST" update --method release
sudo "$PING_RUST" service restart
sudo "$PING_RUST" service status
sudo "$PING_RUST" logs --lines 20
sudo "$PING_RUST" info
sudo systemctl is-active --quiet shoes.service
sudo ss -H -lnt | awk '{print $4}' | grep -Eq "(^|:)${REALITY_PORT}$"
sudo ss -H -lun | awk '{print $4}' | grep -Eq '(^|:)24444$'
sudo ss -H -lun | awk '{print $4}' | grep -Eq '(^|:)24445$'
sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)24446$'
sudo ss -H -lnt | awk '{print $4}' | grep -Eq '(^|:)24447$'
for port in 25451 25452 25453 25454 25455; do
sudo ss -H -lnt | awk '{print $4}' | grep -Eq "(^|:)${port}$"
done
- name: Record acceptance summary
shell: bash
run: |
{
echo '### Ubuntu 24.04 acceptance'
echo
echo "- Public crates.io version: $PUBLIC_VERSION"
echo "- Reality deployment: ${{ steps.reality.outputs.elapsed }} seconds"
echo '- systemd: enabled, active, restart verified'
echo '- Protocol listeners: all ten menu presets'
echo '- Client exports: all ten menu presets to Clash Meta, sing-box, Nekobox'
echo '- Operations: logs, backup, restore, update, restart, info'
} >> "$GITHUB_STEP_SUMMARY"
- name: Uninstall and verify cleanup
if: always()
shell: bash
run: |
set +e
if [ -n "${PING_RUST:-}" ] && [ -x "$PING_RUST" ]; then
sudo "$PING_RUST" uninstall --purge
fi
sudo systemctl disable --now shoes.service 2>/dev/null || true
sudo rm -f /etc/systemd/system/shoes.service /usr/local/bin/shoes
sudo rm -f /run/ping-rust-fail-once.sh /dev/shm/ping-rust-activation-fault
sudo rm -rf /etc/systemd/system/shoes.service.d
rm -f "${SB:-}"
sudo rm -rf /etc/shoes
sudo systemctl daemon-reload
set -e
test ! -e /usr/local/bin/shoes
test ! -e /etc/shoes
test ! -e /etc/systemd/system/shoes.service