@ユメイ9 月前
12/17
00:54
查看状态:/usr/local/sbin/vps-proxy-tune status
bash <<'VPS_INSTALL'
set -e
if [ "$(id -u)" -ne 0 ]; then
echo "请先执行 sudo -i 切换到 root,再粘贴本段。"
exit 1
fi
mkdir -p /usr/local/sbin
VPS_SCRIPT_TMP=$(mktemp /usr/local/sbin/.vps-proxy-tune.XXXXXX)
trap 'rm -f "$VPS_SCRIPT_TMP"' EXIT
cat > "$VPS_SCRIPT_TMP" <<'VPS_SCRIPT'
#!/usr/bin/env bash
# VPS proxy network tuning v2.0.0
# Debian 10+ / Ubuntu 18.04+ / Bash 4.4+
ETC=/etc
PROC=/proc
STATE=/var/lib/vps-proxy-tune
RUN=/run
CONF_NAME=99-zz-vps-proxy-tune.conf
PROFILE=balanced
CC=auto
QDISC=fq
TFO=keep
NOFILE=65536
BUFFER_MIB=0
SERVICE_MODE=auto
ALLOW_CONTAINER=0
ACTION=plan
TXN=
ACTIVE_TXN=0
SYSTEMD=0
SKIPPED=0
declare -a SERVICES=() KEYS=()
declare -A VALUES=() SEEN_SERVICES=()
info() { printf '[INFO] %s\n' "$*"; }
warn() { printf '[WARN] %s\n' "$*" >&2; }
die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
normalize() { awk '{$1=$1; print}'; }
read_key() { sysctl -n "$1" 2>/dev/null | normalize; }
has_cc() {
[[ " $(read_key net.ipv4.tcp_available_congestion_control) " == *" $1 "* ]]
}
usage() {
cat <<'HELP'
VPS 代理网络调优
用法:
vps-proxy-tune plan 只读预览
vps-proxy-tune apply 应用配置
vps-proxy-tune status 查看状态
vps-proxy-tune restore 撤销最近一次应用
选项:
--profile balanced|throughput 默认 balanced
--buffer-mib 1..256 显式设置缓冲上限 MiB
--cc auto|bbr|cubic|keep 默认 auto
--qdisc fq|keep 默认 fq
--tfo keep|on|off 默认 keep
--service UNIT 指定服务,可重复
--no-services 不新增服务限制
--nofile 1024..1048576 默认 65536
--allow-container 显式允许容器内尽力应用
示例:
vps-proxy-tune apply --service realm.service
vps-proxy-tune apply --profile throughput --buffer-mib 32
说明:
无参数只读预览;apply/restore 需要 root。
不安装代理软件,不重启代理,不改防火墙、路由、转发和 RPS/XPS。
NOFILE 在服务下次重启后生效;BBR 主要影响新建 TCP 连接。
default_qdisc=fq 不代表当前网卡队列已经被替换。
balanced 按内存分档使用 4/8/16/32 MiB,throughput 翻倍。
缓冲上限可能比原设置低,应结合带宽、RTT 和并发数实测。
keep 表示本次不管理对应键;重新 apply 会重建本工具的 sysctl 文件。
要彻底撤销上次配置,请先 restore,再应用新的参数。
服务限制增量管理;--no-services 不删除上次创建的限制。
外部配置可能在开机后覆盖参数,请重启后用 status 核验。
回滚不卸载内核模块,不重启代理进程。
回滚会覆盖已记录键/文件在 apply 后的人工修改。
HELP
}
number_in_range() {
[[ $1 =~ ^[0-9]{1,10}$ ]] &&
(( 10#$1 >= $2 && 10#$1 <= $3 ))
}
parse_args() {
if (($#)) && [[ $1 != --* ]]; then
ACTION=$1
shift
fi
case "$ACTION" in
help|-h) usage; exit 0 ;;
plan|apply|status|restore) ;;
*) die "未知操作:$ACTION" ;;
esac
while (($#)); do
case "$1" in
--help|-h) usage; exit 0 ;;
--allow-container)
ALLOW_CONTAINER=1
shift
continue
;;
--no-services)
SERVICE_MODE=none
shift
continue
;;
--profile|--cc|--qdisc|--tfo|--nofile|--buffer-mib|--service)
(($# >= 2)) || die "$1 缺少值"
;;
*) die "未知选项:$1" ;;
esac
case "$1" in
--profile) PROFILE=$2 ;;
--cc) CC=$2 ;;
--qdisc) QDISC=$2 ;;
--tfo) TFO=$2 ;;
--nofile)
number_in_range "$2" 1024 1048576 ||
die 'NOFILE 超出范围'
NOFILE=$((10#$2))
;;
--buffer-mib)
number_in_range "$2" 1 256 ||
die 'buffer-mib 超出范围'
BUFFER_MIB=$((10#$2))
;;
--service)
[[ $2 =~ ^[a-zA-Z0-9_:][a-zA-Z0-9_.:@-]*\.service$ &&
$2 != *@.service ]] ||
die '服务名须为具体 .service 单元'
SERVICES+=("$2")
;;
esac
shift 2
done
case "$PROFILE" in
balanced|throughput) ;;
*) die '未知 profile' ;;
esac
case "$CC" in
auto|bbr|cubic|keep) ;;
*) die '未知 cc' ;;
esac
case "$QDISC" in
fq|keep) ;;
*) die '未知 qdisc' ;;
esac
case "$TFO" in
keep|on|off) ;;
*) die '未知 tfo' ;;
esac
if ((${#SERVICES[@]})); then
[[ $SERVICE_MODE != none ]] ||
die '--service 与 --no-services 不可同时使用'
SERVICE_MODE=explicit
fi
}
detect() {
local ID=unknown PRETTY_NAME=unknown VERSION_ID=unknown
local unit status
[[ -r $ETC/os-release ]] || die '缺少 /etc/os-release'
. "$ETC/os-release"
case "$ID" in
debian|ubuntu) ;;
*) die "仅面向 Debian / Ubuntu;检测为 $ID" ;;
esac
info "$PRETTY_NAME | kernel $(uname -r) | $(uname -m)"
RAM_MB=$(awk '/^MemTotal:/ {printf "%.0f", $2/1024}' "$PROC/meminfo")
[[ $RAM_MB =~ ^[0-9]+$ ]] || die '无法读取内存容量'
info "内存 ${RAM_MB} MiB;按实际内核能力检测"
CONTAINER=0
if command -v systemd-detect-virt >/dev/null 2>&1 &&
systemd-detect-virt --container --quiet; then
CONTAINER=1
elif [[ -e /.dockerenv || -e /run/.containerenv || -e $PROC/vz ]]; then
CONTAINER=1
fi
if ((CONTAINER)); then
warn '容器可能共享宿主内核或受 cgroup 限制'
RAM_MB=256
if [[ $ACTION == apply && $ALLOW_CONTAINER == 0 ]]; then
die '容器应用须显式传入 --allow-container'
fi
fi
if [[ -d $RUN/systemd/system ]] &&
command -v systemctl >/dev/null 2>&1; then
SYSTEMD=1
fi
if [[ $SERVICE_MODE == explicit && $SYSTEMD == 0 ]]; then
die '显式服务配置需要正在运行的 systemd'
fi
if ((SYSTEMD)) && [[ $SERVICE_MODE == auto ]]; then
while read -r unit _; do
case "$unit" in
realm.service|realm-server.service|realm@?*.service|shadowsocks-rust.service|shadowsocks-rust@?*.service|shadowsocks.service|shadowsocks@?*.service|ssserver.service|ssserver@?*.service|xray.service|xray@?*.service|sing-box.service|sing-box@?*.service|hysteria-server.service|hysteria-server@?*.service|tuic.service)
SERVICES+=("$unit")
;;
esac
done < <(
systemctl list-unit-files --type=service \
--no-legend --no-pager 2>/dev/null
systemctl list-units --type=service --all \
--no-legend --no-pager --plain 2>/dev/null
)
fi
local -a valid=()
for unit in "${SERVICES[@]}"; do
[[ -z ${SEEN_SERVICES[$unit]:-} ]] || continue
SEEN_SERVICES[$unit]=1
status=$(systemctl show "$unit" -p LoadState 2>/dev/null) ||
status=
if [[ $status != LoadState=loaded ]]; then
[[ $SERVICE_MODE != explicit ]] ||
die "服务未加载或不存在:$unit"
continue
fi
valid+=("$unit")
done
SERVICES=("${valid[@]}")
if [[ -e $ETC/sysctl.d/99-ss-optimization.conf ||
-e $ETC/systemd/system/ss-network-tuning.service ]]; then
warn '检测到原版 ss-network 配置'
[[ $ACTION != apply ]] ||
die '请先核查旧版备份、停用旧调优服务并处理旧配置,避免两套配置相互覆盖'
fi
}
add_key() {
local old
if old=$(read_key "$1") && [[ -n $old ]]; then
KEYS+=("$1")
VALUES[$1]=$2
else
warn "内核未提供 $1,跳过"
fi
}
raise_key() {
local old
old=$(read_key "$1") || return 0
[[ $old =~ ^[0-9]+$ ]] || return 0
if ((old > $2)); then
add_key "$1" "$old"
else
add_key "$1" "$2"
fi
}
buffer_triplet() {
local key=$1 cap=$2 low middle high
read -r low middle high <<< "$(read_key "$key")"
if [[ $low =~ ^[0-9]+$ &&
$middle =~ ^[0-9]+$ &&
$high =~ ^[0-9]+$ ]]; then
((cap >= middle)) || cap=$middle
((cap >= low)) || cap=$low
add_key "$key" "$low $middle $cap"
fi
}
build_plan() {
local mib cap fast
KEYS=()
VALUES=()
if ((RAM_MB <= 512)); then
mib=4
elif ((RAM_MB <= 1024)); then
mib=8
elif ((RAM_MB <= 4096)); then
mib=16
else
mib=32
fi
if [[ $PROFILE == throughput ]]; then
mib=$((mib*2))
fi
if ((BUFFER_MIB)); then
mib=$BUFFER_MIB
fi
cap=$((mib*1024*1024))
info "socket 上限:${mib} MiB;不是预分配内存"
add_key net.core.rmem_max "$cap"
add_key net.core.wmem_max "$cap"
buffer_triplet net.ipv4.tcp_rmem "$cap"
buffer_triplet net.ipv4.tcp_wmem "$cap"
add_key net.ipv4.tcp_moderate_rcvbuf 1
add_key net.ipv4.tcp_window_scaling 1
add_key net.ipv4.tcp_sack 1
raise_key net.core.somaxconn 4096
raise_key net.ipv4.tcp_max_syn_backlog 4096
case "$CC" in
auto)
if has_cc bbr; then
add_key net.ipv4.tcp_congestion_control bbr
else
info 'BBR 未注册;apply 会尝试加载模块,不可用时保留原算法'
fi
;;
bbr|cubic)
if has_cc "$CC"; then
add_key net.ipv4.tcp_congestion_control "$CC"
elif [[ $ACTION == plan ]]; then
warn "$CC 当前未注册,apply 将探测模块"
else
die "内核未提供要求的 $CC"
fi
;;
esac
[[ $QDISC != fq ]] || add_key net.core.default_qdisc fq
if [[ $TFO != keep ]]; then
fast=$(read_key net.ipv4.tcp_fastopen) || fast=
if [[ $fast =~ ^[0-9]+$ ]]; then
if [[ $TFO == on ]]; then
fast=$((fast | 3))
else
fast=0
fi
add_key net.ipv4.tcp_fastopen "$fast"
fi
fi
}
show_plan() {
local key unit
printf '\n%-42s %-24s %s\n' 'sysctl' 'current' 'planned'
for key in "${KEYS[@]}"; do
printf '%-42s %-24s %s\n' \
"$key" "$(read_key "$key")" "${VALUES[$key]}"
done
for unit in "${SERVICES[@]}"; do
info "$unit: soft NOFILE 至少 $NOFILE,保留已有更高限制"
done
info '预览不验证写权限;apply 逐项写入并回读'
info 'RPS/XPS、转发、MTU 等保持现状'
}
save_file() {
local file=$1
[[ ! -L $file ]] || die "拒绝覆盖符号链接:$file"
if [[ -e $file ]]; then
[[ -f $file ]] || die "不是常规文件:$file"
mkdir -p "$TXN/files$(dirname "$file")"
cp -a -- "$file" "$TXN/files$file"
printf 'present\t%s\n' "$file" >> "$TXN/files.tsv"
else
printf 'absent\t%s\n' "$file" >> "$TXN/files.tsv"
fi
}
atomic_file() {
local source=$1 destination=$2 tmp
mkdir -p "$(dirname "$destination")"
tmp=$(mktemp "${destination}.tmp.XXXXXX")
if ! install -m 0644 -- "$source" "$tmp" ||
! mv -f -- "$tmp" "$destination"; then
rm -f -- "$tmp"
return 1
fi
}
restore_txn() {
local dir=$1 kind file key value current failure=0
while IFS=$'\t' read -r kind file; do
[[ -n $file ]] || continue
if [[ $kind == present ]]; then
if ! cp -a --remove-destination -- \
"$dir/files$file" "$file"; then
failure=1
fi
elif [[ $kind == absent ]]; then
if ! rm -f -- "$file"; then
failure=1
fi
else
warn "无效备份记录:$kind"
failure=1
fi
done < "$dir/files.tsv"
while IFS=$'\t' read -r key value; do
[[ -n $key ]] || continue
current=$(read_key "$key") || current=
[[ $current != "$value" ]] || continue
if ! sysctl -q -w "$key=$value"; then
warn "无法恢复 $key"
failure=1
continue
fi
if [[ $(read_key "$key") != "$value" ]]; then
warn "恢复后回读不一致:$key"
failure=1
fi
done < "$dir/sysctl.tsv"
if [[ -s $dir/services.txt ]] && ((SYSTEMD)); then
if ! systemctl daemon-reload; then
failure=1
fi
fi
return "$failure"
}
on_exit() {
local code=$? parent
trap - EXIT INT TERM
if ((ACTIVE_TXN)); then
warn "应用未完成,回滚本次修改:$TXN"
if restore_txn "$TXN"; then
parent=$(cat "$TXN/parent")
if [[ -n $parent ]]; then
printf '%s\n' "$parent" > "$STATE/latest.tmp"
mv -f "$STATE/latest.tmp" "$STATE/latest"
else
rm -f "$STATE/latest"
fi
rm -f "$STATE/pending"
else
warn '自动回滚未完全成功,请重新运行 restore'
fi
((code != 0)) || code=1
fi
exit "$code"
}
lock_state() {
mkdir -p "$STATE"
[[ ! -L $STATE && $(stat -c %u "$STATE") == 0 ]] ||
die '状态目录必须为 root 所有的非符号链接目录'
chmod 0700 "$STATE"
exec 9>"$STATE/lock"
flock -n 9 || die '另一调优或恢复任务正在运行'
}
set_limits() {
local unit file soft hard limit nr
nr=$(read_key fs.nr_open) || die '无法读取 fs.nr_open'
number_in_range "$nr" 1024 2147483647 ||
die '无效 fs.nr_open'
((NOFILE <= nr)) ||
die "--nofile 大于内核 fs.nr_open=$nr"
for unit in "${SERVICES[@]}"; do
soft=$(systemctl show "$unit" -p LimitNOFILESoft)
soft=${soft#*=}
hard=$(systemctl show "$unit" -p LimitNOFILE)
hard=${hard#*=}
[[ $soft != 18446744073709551615 ]] || soft=infinity
[[ $hard != 18446744073709551615 ]] || hard=infinity
[[ $soft =~ ^[0-9]+$ || $soft == infinity ]] ||
die "$unit 无法读取 soft NOFILE"
[[ $hard =~ ^[0-9]+$ || $hard == infinity ]] ||
die "$unit 无法读取 hard NOFILE"
if [[ $soft != infinity ]] && ((soft < NOFILE)); then
soft=$NOFILE
fi
if [[ $hard != infinity ]]; then
if [[ $soft == infinity ]]; then
hard=infinity
elif ((hard < soft)); then
hard=$soft
fi
fi
limit="$soft:$hard"
file="$ETC/systemd/system/$unit.d/90-vps-proxy-tune.conf"
save_file "$file"
printf '%s\n' "$unit" >> "$TXN/services.txt"
printf '[Service]\nLimitNOFILE=%s\n' "$limit" > "$TXN/dropin"
atomic_file "$TXN/dropin" "$file"
done
if ((${#SERVICES[@]})); then
systemctl daemon-reload
for unit in "${SERVICES[@]}"; do
soft=$(systemctl show "$unit" -p LimitNOFILESoft)
soft=${soft#*=}
[[ $soft != 18446744073709551615 ]] || soft=infinity
if [[ $soft != infinity ]] &&
{ [[ ! $soft =~ ^[0-9]+$ ]] || ((soft < NOFILE)); }; then
die "$unit 存在更高优先级覆盖,NOFILE 未达到目标"
fi
done
fi
}
apply_plan() {
local key old value now
lock_state
[[ ! -e $STATE/pending ]] ||
die '存在中断任务,请先运行 vps-proxy-tune restore'
TXN=$(mktemp -d "$STATE/txn-XXXXXXXX")
: > "$TXN/files.tsv"
: > "$TXN/sysctl.tsv"
: > "$TXN/services.txt"
if [[ -f $STATE/latest ]]; then
cp "$STATE/latest" "$TXN/parent"
else
: > "$TXN/parent"
fi
printf '%s\n' "${TXN##*/}" > "$STATE/pending"
ACTIVE_TXN=1
trap on_exit EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
save_file "$ETC/sysctl.d/$CONF_NAME"
if ((CONTAINER == 0)) &&
command -v modprobe >/dev/null 2>&1; then
case "$CC" in
auto|bbr)
has_cc bbr ||
modprobe tcp_bbr 2>>"$TXN/modules.log" || true
;;
cubic)
has_cc cubic ||
modprobe tcp_cubic 2>>"$TXN/modules.log" || true
;;
esac
[[ $QDISC != fq ]] ||
modprobe sch_fq 2>>"$TXN/modules.log" || true
fi
build_plan
printf '# Managed by vps-proxy-tune; verified keys only.\n' \
> "$TXN/new.conf"
for key in "${KEYS[@]}"; do
old=$(read_key "$key") || {
warn "$key 不可读,跳过"
SKIPPED=$((SKIPPED+1))
continue
}
value=${VALUES[$key]}
printf '%s\t%s\n' "$key" "$old" >> "$TXN/sysctl.tsv"
if [[ $old == "$value" ]] ||
sysctl -q -w "$key=$value" 2>>"$TXN/sysctl-errors.log"; then
now=$(read_key "$key") || now=
if [[ $now == "$value" ]]; then
printf '%s = %s\n' "$key" "$value" >> "$TXN/new.conf"
info "$key = $value"
continue
fi
fi
warn "$key 写入失败或回读不一致,不持久化"
now=$(read_key "$key") || now=
if [[ $now != "$old" ]]; then
sysctl -q -w "$key=$old" ||
die "无法恢复失败项 $key"
[[ $(read_key "$key") == "$old" ]] ||
die "恢复失败项回读不一致:$key"
fi
if [[ $key == net.ipv4.tcp_congestion_control && $CC != auto ]]; then
die '显式要求的拥塞控制无法生效'
fi
SKIPPED=$((SKIPPED+1))
done
atomic_file "$TXN/new.conf" "$ETC/sysctl.d/$CONF_NAME"
if ((${#SERVICES[@]})); then
set_limits
fi
printf '%s\n' "${TXN##*/}" > "$STATE/latest.tmp"
mv -f "$STATE/latest.tmp" "$STATE/latest"
rm -f "$STATE/pending"
ACTIVE_TXN=0
trap - EXIT INT TERM
info "配置已保存,跳过 $SKIPPED 项"
info "备份:$TXN"
info '未重启代理服务;NOFILE 下次重启生效'
info 'default_qdisc=fq 不等于当前网卡已经使用 fq'
info '查看状态:vps-proxy-tune status'
info '撤销本次:vps-proxy-tune restore'
}
restore_latest() {
local name dir parent
lock_state
if [[ -f $STATE/pending ]]; then
name=$(cat "$STATE/pending")
elif [[ -f $STATE/latest ]]; then
name=$(cat "$STATE/latest")
else
die '没有可恢复的本工具备份'
fi
[[ $name =~ ^txn-[a-zA-Z0-9]+$ ]] || die '无效备份编号'
dir="$STATE/$name"
[[ -f $dir/parent &&
-f $dir/files.tsv &&
-f $dir/sysctl.tsv ]] || die '备份不完整'
if ! restore_txn "$dir"; then
die "恢复未完全成功,保留备份以便重试:$dir"
fi
parent=$(cat "$dir/parent")
if [[ -n $parent ]]; then
printf '%s\n' "$parent" > "$STATE/latest.tmp"
mv -f "$STATE/latest.tmp" "$STATE/latest"
else
rm -f "$STATE/latest"
fi
rm -f "$STATE/pending"
info "已恢复本次应用前的文件及 sysctl:$name"
info '如曾重启代理启用 NOFILE,需再次重启才能恢复进程限制'
}
status_report() {
local key unit pid
for key in \
net.ipv4.tcp_available_congestion_control \
net.ipv4.tcp_congestion_control \
net.core.default_qdisc \
net.core.rmem_max \
net.core.wmem_max \
net.ipv4.tcp_rmem \
net.ipv4.tcp_wmem \
net.ipv4.tcp_fastopen \
net.ipv4.ip_forward \
net.ipv6.conf.all.forwarding; do
printf '%-45s %s\n' \
"$key" "$(read_key "$key" || printf unavailable)"
done
printf '\n--- IPv4 / IPv6 默认路由 ---\n'
ip -4 route show default || true
ip -6 route show default || true
printf '\n--- 网卡实际 qdisc ---\n'
if command -v tc >/dev/null 2>&1; then
tc -s qdisc show || true
fi
for unit in "${SERVICES[@]}"; do
printf '\n--- %s ---\n' "$unit"
systemctl show "$unit" \
-p LimitNOFILE -p LimitNOFILESoft -p MainPID || true
pid=$(systemctl show "$unit" -p MainPID)
pid=${pid#*=}
if [[ $pid =~ ^[1-9][0-9]*$ && -r $PROC/$pid/limits ]]; then
awk '/^Max open files/ {print "运行中进程:", $0}' \
"$PROC/$pid/limits"
fi
done
}
main() {
set -Eeuo pipefail
umask 077
export LC_ALL=C
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
(( BASH_VERSINFO[0] > 4 ||
(BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )) ||
die '需要 Bash 4.4+'
parse_args "$@"
local cmd
for cmd in sysctl ip awk uname; do
command -v "$cmd" >/dev/null ||
die "缺少命令:$cmd"
done
if [[ $ACTION == apply || $ACTION == restore ]]; then
((EUID == 0)) || die 'apply / restore 需要 root'
for cmd in flock mktemp install cp mv rm mkdir chmod stat; do
command -v "$cmd" >/dev/null ||
die "缺少命令:$cmd"
done
fi
if [[ $ACTION == restore ]]; then
if [[ -d $RUN/systemd/system ]] &&
command -v systemctl >/dev/null; then
SYSTEMD=1
fi
restore_latest
return
fi
detect
case "$ACTION" in
plan) build_plan; show_plan ;;
status) status_report ;;
apply) apply_plan ;;
esac
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi
VPS_SCRIPT
bash -n "$VPS_SCRIPT_TMP"
chmod 0755 "$VPS_SCRIPT_TMP"
mv -f -- "$VPS_SCRIPT_TMP" /usr/local/sbin/vps-proxy-tune
bash /usr/local/sbin/vps-proxy-tune apply
VPS_INSTALL
