206 lines
7.1 KiB
Bash
206 lines
7.1 KiB
Bash
#!/bin/bash
|
||
|
||
# 查找随机空闲端口
|
||
# 用途:
|
||
# 在指定端口范围内,随机选择一个当前未被监听(TCP/UDP)的端口
|
||
# 参数:
|
||
# $1: 起始端口(默认 10001)
|
||
# $2: 结束端口(默认 65535)
|
||
# $3: 最大随机尝试次数(默认 100)
|
||
find_free_port() {
|
||
local START="${1:-10001}"
|
||
local MAX="${2:-65535}"
|
||
local ATTEMPTS="${3:-100}"
|
||
|
||
# ------------------------------------------------------------
|
||
# 收集当前正在监听的端口(TCP + UDP)
|
||
# 优先使用 ss,其次 netstat
|
||
# ------------------------------------------------------------
|
||
local USED_PORTS RAW
|
||
if command -v ss >/dev/null 2>&1; then
|
||
# ss 输出示例:
|
||
# LISTEN 0 128 0.0.0.0:22
|
||
RAW=$(ss -lntu 2>/dev/null || true)
|
||
USED_PORTS=$(printf "%s\n" "$RAW" \
|
||
| awk '{print $5}' \
|
||
| sed -E 's/.*[:]//g')
|
||
elif command -v netstat >/dev/null 2>&1; then
|
||
# netstat 输出示例:
|
||
# tcp 0 0 0.0.0.0:22
|
||
RAW=$(netstat -lntu 2>/dev/null || true)
|
||
USED_PORTS=$(printf "%s\n" "$RAW" \
|
||
| awk '{print $4}' \
|
||
| sed -E 's/.*[:]//g')
|
||
else
|
||
echo "Error: ss or netstat is required to check listening ports." >&2
|
||
return 2
|
||
fi
|
||
|
||
# ------------------------------------------------------------
|
||
# 将已占用端口存入关联数组,便于 O(1) 判断
|
||
# 需要 bash 4+
|
||
# ------------------------------------------------------------
|
||
declare -A USED_MAP
|
||
local P
|
||
for P in $USED_PORTS; do
|
||
# 过滤非数字字段(如 *、:::)
|
||
if [[ $P =~ ^[0-9]+$ ]]; then
|
||
USED_MAP["$P"]=1
|
||
fi
|
||
done
|
||
|
||
# ------------------------------------------------------------
|
||
# 随机尝试若干次
|
||
# 每次随机生成一个端口,只要未被监听就立即返回
|
||
# ------------------------------------------------------------
|
||
local PORT I
|
||
for ((I=0; I<ATTEMPTS; I++)); do
|
||
# 在 [start, max] 范围内生成随机端口
|
||
PORT=$(( RANDOM % (MAX - START + 1) + START ))
|
||
|
||
# 如果端口不在已监听表中,则认为可用
|
||
if [[ -z "${USED_MAP[$PORT]}" ]]; then
|
||
echo "$PORT"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# ------------------------------------------------------------
|
||
# 多次随机尝试后仍未找到可用端口
|
||
# 通常意味着端口范围过小或已被大量占用
|
||
# ------------------------------------------------------------
|
||
echo "Error: no free port found after $ATTEMPTS random attempts." >&2
|
||
return 1
|
||
}
|
||
|
||
# ==========================================
|
||
# 函数名: port_manage
|
||
# 功能: 开放端口和关闭端口
|
||
# 参数1: allow 或 deny (操作类型)
|
||
# 参数2: 端口号
|
||
# --- 使用示例 ---
|
||
# 开启端口 8888
|
||
# port_manage allow 8888
|
||
|
||
# 关闭端口 8888
|
||
# port_manage deny 8888
|
||
# ==========================================
|
||
port_manage() {
|
||
local ACTION=$1
|
||
local PORT=$2
|
||
|
||
if [[ -z "$PORT" ]]; then
|
||
echo "错误: 未提供端口号"
|
||
return 1
|
||
fi
|
||
|
||
# 统一转换为小写,增强鲁棒性
|
||
ACTION=$(echo "$ACTION" | tr '[:upper:]' '[:lower:]')
|
||
|
||
echo "--- 正在对端口 $PORT 执行 $ACTION 操作 ---"
|
||
|
||
# 1. 防火墙 (Firewall) 逻辑处理
|
||
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
|
||
# CentOS/RHEL/Fedora (firewalld)
|
||
if [ "$ACTION" == "allow" ]; then
|
||
sudo firewall-cmd --zone=public --add-port=${PORT}/tcp --permanent
|
||
sudo firewall-cmd --zone=public --add-port=${PORT}/udp --permanent
|
||
elif [ "$ACTION" == "deny" ]; then
|
||
sudo firewall-cmd --zone=public --remove-port=${PORT}/tcp --permanent
|
||
sudo firewall-cmd --zone=public --remove-port=${PORT}/udp --permanent
|
||
fi
|
||
sudo firewall-cmd --reload
|
||
echo "[OK] firewalld 规则已更新 ($ACTION)"
|
||
|
||
elif command -v ufw >/dev/null 2>&1 && systemctl is-active --quiet ufw; then
|
||
# Ubuntu/Debian (ufw)
|
||
if [ "$ACTION" == "allow" ]; then
|
||
sudo ufw allow ${PORT}/tcp
|
||
sudo ufw allow ${PORT}/udp
|
||
elif [ "$ACTION" == "deny" ]; then
|
||
sudo ufw delete allow ${PORT}/tcp
|
||
sudo ufw delete allow ${PORT}/udp
|
||
fi
|
||
echo "[OK] ufw 规则已更新 ($ACTION)"
|
||
|
||
else
|
||
# 防火墙未启用时,allow 操作直接跳过,deny 操作走 iptables 兜底
|
||
if [ "$ACTION" == "allow" ]; then
|
||
echo "[!] 防火墙未启用,跳过防火墙规则配置"
|
||
else
|
||
# 兜底方案 (iptables) —— 仅 deny 时执行
|
||
sudo iptables -D INPUT -p tcp --dport ${PORT} -j ACCEPT 2>/dev/null
|
||
sudo iptables -D INPUT -p udp --dport ${PORT} -j ACCEPT 2>/dev/null
|
||
echo "[OK] iptables 规则已执行 ($ACTION)"
|
||
fi
|
||
fi
|
||
|
||
# 2. SELinux 逻辑处理(与防火墙状态无关,始终执行)
|
||
|
||
# getenforce 不存在说明未安装 SELinux,直接跳过
|
||
command -v getenforce >/dev/null 2>&1 || return 0
|
||
|
||
# Enforcing 模式才会拦截端口绑定,其他模式无需处理
|
||
local SELINUX_STATUS=$(getenforce)
|
||
[ "$SELINUX_STATUS" == "Enforcing" ] || return 0
|
||
|
||
# semanage 不存在时的兜底方案: 开启全局网络连接开关(粒度较粗)
|
||
if ! command -v semanage >/dev/null 2>&1; then
|
||
if [ "$ACTION" == "allow" ]; then
|
||
echo "[!] 警告: 未找到 semanage,尝试开启全局网络连接开关..."
|
||
sudo setsebool -P httpd_can_network_connect 1
|
||
fi
|
||
return 0
|
||
fi
|
||
|
||
# 使用 semanage 精确管理端口标签
|
||
if [ "$ACTION" == "allow" ]; then
|
||
# 先尝试添加 (-a),若标签已存在则改用修改 (-m)
|
||
sudo semanage port -a -t http_port_t -p tcp ${PORT} 2>/dev/null || \
|
||
sudo semanage port -m -t http_port_t -p tcp ${PORT}
|
||
elif [ "$ACTION" == "deny" ]; then
|
||
sudo semanage port -d -t http_port_t -p tcp ${PORT} 2>/dev/null
|
||
fi
|
||
echo "[OK] SELinux 端口权限已更新 ($ACTION)"
|
||
}
|
||
|
||
# ==========================================
|
||
# 更新 sing-box 配置文件中的监听端口,并开放对应防火墙规则
|
||
# 参数: 端口号(可选,不传则设置一个5000 到 15000的空闲端口)
|
||
# --- 使用示例 ---
|
||
# 指定端口
|
||
# update_port 8080
|
||
# ==========================================
|
||
update_port(){
|
||
local PORT=$1
|
||
# 如果没有提供端口参数,则自动查找一个随机空闲端口
|
||
if [[ -z "$PORT" ]]; then
|
||
PORT=$(find_free_port 5000 15000)
|
||
fi
|
||
|
||
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 脚本文件夹绝对路径
|
||
local CONFIG_DIR="$SCRIPT_DIR/../config"
|
||
modify_json_file "$CONFIG_DIR/config.json" ".inbounds[0].listen_port" "$PORT"
|
||
port_manage allow "$PORT"
|
||
echo "设置端口成功"
|
||
}
|
||
|
||
# 通过交互方式更新端口
|
||
ask_update_port(){
|
||
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
local CONFIG_FILE="$SCRIPT_DIR/../config/config.json"
|
||
|
||
local CURRENT_PORT=$(get_listen_port)
|
||
local NEW_PORT=$(ask_update "请输入新的端口号" "$CURRENT_PORT")
|
||
update_port "$NEW_PORT"
|
||
}
|
||
|
||
# 获取当前配置的监听端口
|
||
get_listen_port(){
|
||
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
local CONFIG_FILE="$SCRIPT_DIR/../config/config.json"
|
||
|
||
local PORT=$(jq -r ".inbounds[0].listen_port" "$CONFIG_FILE")
|
||
echo $PORT
|
||
}
|