1033 lines
29 KiB
Bash
Executable File
1033 lines
29 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -Eeuo pipefail
|
||
|
||
# Debian/Ubuntu MicroSocks multi-instance installer and manager.
|
||
# Status messages go to stderr; socks5:// links and instance lists go to stdout.
|
||
|
||
ACTION="create"
|
||
COUNT=0
|
||
COUNT_SET=0
|
||
START_PORT=1080
|
||
START_PORT_SET=0
|
||
LISTEN_ADDR="0.0.0.0"
|
||
PUBLIC_HOST=""
|
||
COMMON_USER=""
|
||
COMMON_PASS=""
|
||
ALLOW_LIST=""
|
||
ALLOW_ALL=0
|
||
NO_AUTH=0
|
||
AUTO_BIND=0
|
||
REPLACE=0
|
||
OPEN_FIREWALL=0
|
||
NON_INTERACTIVE=0
|
||
DRY_RUN=0
|
||
APT_UPDATED=0
|
||
MICROSOCKS_BIN=""
|
||
WORK_DIR=""
|
||
CONF_DIR="/etc/microsocks"
|
||
ACL_FILE="$CONF_DIR/acl.nft"
|
||
UNIT_FILE="/etc/systemd/system/microsocks@.service"
|
||
SOURCE_BIN="/usr/local/bin/microsocks"
|
||
|
||
INST_LISTEN=""
|
||
INST_USER=""
|
||
INST_PASS=""
|
||
INST_BIND=""
|
||
INST_HOST=""
|
||
INST_ALLOW=""
|
||
INST_FIREWALL=0
|
||
|
||
declare -a BIND_ADDRESSES=()
|
||
declare -a RESOLVED_BINDS=()
|
||
declare -a PORTS=()
|
||
declare -a USERS=()
|
||
declare -a PASSWORDS=()
|
||
declare -a LOCAL_ADDRESSES=()
|
||
declare -a ALLOW_ARGS=()
|
||
declare -a TARGET_PORTS=()
|
||
|
||
|
||
log() {
|
||
printf '[microsocks] %s\n' "$*" >&2
|
||
}
|
||
|
||
|
||
die() {
|
||
printf '[microsocks] 错误: %s\n' "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:
|
||
sudo ./setup_microsocks.sh [create] [选项] 创建代理(默认)
|
||
sudo ./setup_microsocks.sh list [PORT...] 查看实例状态
|
||
sudo ./setup_microsocks.sh links [PORT...] 输出连接字符串;可用 --host 替换主机
|
||
sudo ./setup_microsocks.sh update [PORT...] --allow CIDR | --allow-all | --host HOST
|
||
sudo ./setup_microsocks.sh start|stop|restart [PORT...]
|
||
sudo ./setup_microsocks.sh remove PORT... [-y]
|
||
|
||
创建选项:
|
||
-n, --count N 创建 N 个代理;多个 --bind 时可省略
|
||
--start-port PORT 起始端口,默认 1080
|
||
--listen IP 监听地址,默认 0.0.0.0
|
||
--host HOST 返回链接使用的公网 IP 或域名
|
||
--bind IP 绑定出站源地址;可重复指定
|
||
--auto-bind 自动轮流使用本机所有全局 IP 作为出口
|
||
--allow CIDR 仅允许这些来源 IP/网段连接;可重复指定或用逗号分隔
|
||
--user USER 所有实例使用同一用户名
|
||
--password PASS 所有实例使用同一密码
|
||
--no-auth 仅允许在回环监听地址上关闭认证
|
||
--replace 覆盖相同端口的既有 microsocks 实例
|
||
--open-firewall 自动放行已启用的 UFW/firewalld 端口
|
||
-y, --non-interactive 不询问确认
|
||
--dry-run 仅显示计划和链接,不修改系统
|
||
-h, --help 显示帮助
|
||
EOF
|
||
usage_examples
|
||
}
|
||
|
||
|
||
usage_examples() {
|
||
cat <<'EOF'
|
||
|
||
说明:
|
||
管理命令省略 PORT 时作用于全部实例(remove 除外);start/stop 同时开启/关闭开机自启。
|
||
来源限制写入 nftables 表 inet microsocks,与 UFW/firewalld 叠加生效;本机回环不受限。
|
||
|
||
示例:
|
||
sudo ./setup_microsocks.sh
|
||
sudo ./setup_microsocks.sh --count 3 --start-port 1080
|
||
sudo ./setup_microsocks.sh --bind 10.0.14.56 --bind 10.0.225.167
|
||
sudo ./setup_microsocks.sh --allow 203.0.113.5,198.51.100.0/24
|
||
sudo ./setup_microsocks.sh update 1080 --allow 203.0.113.7
|
||
sudo ./setup_microsocks.sh links
|
||
EOF
|
||
}
|
||
|
||
|
||
need_value() {
|
||
[[ $# -ge 2 && -n ${2:-} ]] || die "$1 缺少参数"
|
||
}
|
||
|
||
|
||
parse_cli() {
|
||
case "${1:-}" in
|
||
create|list|links|update|start|stop|restart|remove) ACTION=$1; shift ;;
|
||
esac
|
||
if [[ $ACTION == create ]]; then
|
||
parse_args "$@"
|
||
else
|
||
parse_manage_args "$@"
|
||
fi
|
||
}
|
||
|
||
|
||
parse_args() {
|
||
while (($#)); do
|
||
case "$1" in
|
||
-n|--count) need_value "$@"; COUNT=$2; COUNT_SET=1; shift 2 ;;
|
||
--start-port) need_value "$@"; START_PORT=$2; START_PORT_SET=1; shift 2 ;;
|
||
--listen) need_value "$@"; LISTEN_ADDR=$2; shift 2 ;;
|
||
--host) need_value "$@"; PUBLIC_HOST=$2; shift 2 ;;
|
||
--bind) need_value "$@"; BIND_ADDRESSES+=("$2"); shift 2 ;;
|
||
--allow) need_value "$@"; ALLOW_ARGS+=("$2"); shift 2 ;;
|
||
--user) need_value "$@"; COMMON_USER=$2; shift 2 ;;
|
||
--password) need_value "$@"; COMMON_PASS=$2; shift 2 ;;
|
||
--auto-bind) AUTO_BIND=1; shift ;;
|
||
--no-auth) NO_AUTH=1; shift ;;
|
||
--replace) REPLACE=1; shift ;;
|
||
--open-firewall) OPEN_FIREWALL=1; shift ;;
|
||
-y|--non-interactive) NON_INTERACTIVE=1; shift ;;
|
||
--dry-run) DRY_RUN=1; shift ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
*) die "未知参数: $1" ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
|
||
parse_manage_args() {
|
||
while (($#)); do
|
||
case "$1" in
|
||
--allow) need_value "$@"; ALLOW_ARGS+=("$2"); shift 2 ;;
|
||
--allow-all) ALLOW_ALL=1; shift ;;
|
||
--host) need_value "$@"; PUBLIC_HOST=$2; shift 2 ;;
|
||
-y|--non-interactive) NON_INTERACTIVE=1; shift ;;
|
||
-h|--help) usage; exit 0 ;;
|
||
-*) die "未知参数: $1" ;;
|
||
*) is_uint "$1" || die "无效的端口: $1"; TARGET_PORTS+=("$1"); shift ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
|
||
prompt_number() {
|
||
local prompt=$1 default=$2 value
|
||
read -r -p "$prompt [$default]: " value
|
||
printf '%s' "${value:-$default}"
|
||
}
|
||
|
||
|
||
interactive_options() {
|
||
[[ -t 0 && $NON_INTERACTIVE -eq 0 ]] || return 0
|
||
local answer
|
||
if [[ $COUNT_SET -eq 0 ]]; then
|
||
COUNT=$(prompt_number "代理数量" 1)
|
||
COUNT_SET=1
|
||
fi
|
||
if [[ $START_PORT_SET -eq 0 ]]; then
|
||
START_PORT=$(prompt_number "起始端口" 1080)
|
||
fi
|
||
if is_uint "$COUNT" && ((COUNT > 1 && ${#BIND_ADDRESSES[@]} == 0)); then
|
||
read -r -p "是否按检测到的本机 IP 自动分配出口?[y/N]: " answer
|
||
if [[ $answer =~ ^[Yy]$ ]]; then AUTO_BIND=1; fi
|
||
fi
|
||
if ((${#ALLOW_ARGS[@]} == 0)); then
|
||
read -r -p "允许连接的来源 IP/CIDR(逗号分隔,留空不限制): " answer
|
||
if [[ -n $answer ]]; then ALLOW_ARGS+=("$answer"); fi
|
||
fi
|
||
}
|
||
|
||
|
||
is_uint() {
|
||
[[ $1 =~ ^[0-9]+$ ]]
|
||
}
|
||
|
||
|
||
valid_token() {
|
||
[[ $1 =~ ^[A-Za-z0-9._~-]{1,128}$ ]]
|
||
}
|
||
|
||
|
||
valid_ip() {
|
||
python3 - "$1" <<'PY' >/dev/null 2>&1
|
||
import ipaddress
|
||
import sys
|
||
ipaddress.ip_address(sys.argv[1])
|
||
PY
|
||
}
|
||
|
||
|
||
valid_host() {
|
||
local host=${1#[}
|
||
host=${host%]}
|
||
valid_ip "$host" && return 0
|
||
[[ $host =~ ^[A-Za-z0-9]([A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$ ]]
|
||
}
|
||
|
||
|
||
# Prints the merged networks, or the first invalid value on failure.
|
||
normalize_cidrs() {
|
||
python3 - "$@" <<'PY'
|
||
import ipaddress
|
||
import sys
|
||
|
||
values = [v.strip() for arg in sys.argv[1:] for v in arg.split(",") if v.strip()]
|
||
if not values:
|
||
sys.exit(1)
|
||
networks = []
|
||
for value in values:
|
||
try:
|
||
networks.append(ipaddress.ip_network(value, strict=False))
|
||
except ValueError:
|
||
print(value)
|
||
sys.exit(1)
|
||
merged = []
|
||
for version in (4, 6):
|
||
family = [n for n in networks if n.version == version]
|
||
merged += [str(n) for n in ipaddress.collapse_addresses(family)]
|
||
print(" ".join(merged))
|
||
PY
|
||
}
|
||
|
||
|
||
prepare_allow_list() {
|
||
((${#ALLOW_ARGS[@]} > 0)) || return 0
|
||
local output
|
||
output=$(normalize_cidrs "${ALLOW_ARGS[@]}") || die "无效的 --allow 地址: ${output:-空}"
|
||
ALLOW_LIST=$output
|
||
}
|
||
|
||
|
||
validate_options() {
|
||
if [[ $COUNT_SET -eq 0 ]]; then
|
||
COUNT=$((${#BIND_ADDRESSES[@]} > 1 ? ${#BIND_ADDRESSES[@]} : 1))
|
||
fi
|
||
is_uint "$COUNT" && ((COUNT >= 1)) || die "代理数量必须是正整数"
|
||
is_uint "$START_PORT" || die "起始端口必须是整数"
|
||
((START_PORT >= 1024 && START_PORT + COUNT - 1 <= 65535)) || die "端口范围必须在 1024-65535"
|
||
((AUTO_BIND == 0 || ${#BIND_ADDRESSES[@]} == 0)) || die "--auto-bind 不能和 --bind 同时使用"
|
||
[[ -z $COMMON_USER && -z $COMMON_PASS ]] || [[ -n $COMMON_USER && -n $COMMON_PASS ]] || die "--user 和 --password 必须同时指定"
|
||
[[ -z $COMMON_USER ]] || valid_token "$COMMON_USER" || die "用户名只能使用字母、数字和 ._~-"
|
||
[[ -z $COMMON_PASS ]] || valid_token "$COMMON_PASS" || die "密码只能使用字母、数字和 ._~-"
|
||
[[ -z $PUBLIC_HOST ]] || valid_host "$PUBLIC_HOST" || die "--host 格式无效"
|
||
valid_ip "$LISTEN_ADDR" || die "--listen 必须是 IPv4 或 IPv6 地址"
|
||
prepare_allow_list
|
||
}
|
||
|
||
|
||
validate_auth_mode() {
|
||
[[ $NO_AUTH -eq 0 ]] && return 0
|
||
[[ -z $COMMON_USER && -z $COMMON_PASS ]] || die "--no-auth 不能与用户名密码同时使用"
|
||
case "$LISTEN_ADDR" in
|
||
127.*|::1) ;;
|
||
*) die "为防止开放代理,--no-auth 仅允许监听回环地址" ;;
|
||
esac
|
||
}
|
||
|
||
|
||
validate_manage_options() {
|
||
local allow_used=$((${#ALLOW_ARGS[@]} > 0 || ALLOW_ALL == 1))
|
||
((ALLOW_ALL == 0 || ${#ALLOW_ARGS[@]} == 0)) || die "--allow 不能和 --allow-all 同时使用"
|
||
case "$ACTION" in
|
||
update)
|
||
((allow_used == 1)) || [[ -n $PUBLIC_HOST ]] ||
|
||
die "update 需要 --allow、--allow-all 或 --host"
|
||
;;
|
||
links) ((allow_used == 0)) || die "links 只支持 --host 选项" ;;
|
||
*)
|
||
((allow_used == 0)) || die "$ACTION 不支持 --allow"
|
||
[[ -z $PUBLIC_HOST ]] || die "$ACTION 不支持 --host"
|
||
;;
|
||
esac
|
||
[[ $ACTION != remove || ${#TARGET_PORTS[@]} -gt 0 ]] || die "remove 必须指定端口"
|
||
[[ -z $PUBLIC_HOST ]] || valid_host "$PUBLIC_HOST" || die "--host 格式无效"
|
||
prepare_allow_list
|
||
}
|
||
|
||
|
||
detect_os() {
|
||
[[ -r /etc/os-release ]] || die "无法识别操作系统"
|
||
# shellcheck disable=SC1091
|
||
source /etc/os-release
|
||
local family="${ID:-} ${ID_LIKE:-}"
|
||
[[ $family == *debian* || $family == *ubuntu* ]] || die "仅支持 Debian/Ubuntu 系统"
|
||
command -v systemctl >/dev/null || die "未检测到 systemd"
|
||
[[ $(ps -p 1 -o comm= 2>/dev/null) == systemd ]] || die "PID 1 不是 systemd"
|
||
log "系统: ${PRETTY_NAME:-$ID}; 架构: $(uname -m)"
|
||
}
|
||
|
||
|
||
apt_update_once() {
|
||
[[ $APT_UPDATED -eq 1 ]] && return 0
|
||
log "更新 APT 索引"
|
||
DEBIAN_FRONTEND=noninteractive apt-get update >&2
|
||
APT_UPDATED=1
|
||
}
|
||
|
||
|
||
ensure_base_tools() {
|
||
local missing=() command
|
||
for command in curl openssl python3 ip ss flock; do
|
||
command -v "$command" >/dev/null || missing+=("$command")
|
||
done
|
||
((${#missing[@]} == 0)) && return 0
|
||
[[ $DRY_RUN -eq 0 ]] || die "dry-run 缺少命令: ${missing[*]}"
|
||
apt_update_once
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y curl openssl python3 iproute2 util-linux ca-certificates >&2
|
||
}
|
||
|
||
|
||
install_from_source() {
|
||
apt_update_once
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential ca-certificates >&2
|
||
log "从上游源码编译 microsocks"
|
||
rm -rf -- "$WORK_DIR/microsocks"
|
||
git clone --depth 1 https://github.com/rofl0r/microsocks.git "$WORK_DIR/microsocks"
|
||
make -C "$WORK_DIR/microsocks" >&2
|
||
install -m 0755 "$WORK_DIR/microsocks/microsocks" "$SOURCE_BIN"
|
||
MICROSOCKS_BIN=$SOURCE_BIN
|
||
}
|
||
|
||
|
||
install_microsocks() {
|
||
if command -v microsocks >/dev/null; then
|
||
MICROSOCKS_BIN=$(command -v microsocks)
|
||
return 0
|
||
fi
|
||
apt_update_once
|
||
log "尝试从 APT 安装 microsocks"
|
||
if DEBIAN_FRONTEND=noninteractive apt-get install -y microsocks >&2; then
|
||
MICROSOCKS_BIN=$(command -v microsocks)
|
||
return 0
|
||
fi
|
||
install_from_source
|
||
}
|
||
|
||
|
||
oci_public_ip() {
|
||
curl -fsS --max-time 2 -H 'Authorization: Bearer Oracle' \
|
||
http://169.254.169.254/opc/v2/vnics/ 2>/dev/null | python3 -c '
|
||
import json, sys
|
||
items = json.load(sys.stdin)
|
||
print(next((x.get("publicIp") for x in items if x.get("publicIp")), ""))
|
||
' 2>/dev/null
|
||
}
|
||
|
||
|
||
first_global_ipv6() {
|
||
ip -o -6 addr show scope global | awk '{sub(/\/.*/, "", $4); print $4; exit}'
|
||
}
|
||
|
||
|
||
detect_public_host() {
|
||
[[ -n $PUBLIC_HOST ]] && return 0
|
||
case "$LISTEN_ADDR" in
|
||
127.*|::1) PUBLIC_HOST=$LISTEN_ADDR; return 0 ;;
|
||
esac
|
||
if [[ $LISTEN_ADDR == *:* ]]; then
|
||
[[ $LISTEN_ADDR == "::" ]] && PUBLIC_HOST=$(first_global_ipv6 || true) ||
|
||
PUBLIC_HOST=$LISTEN_ADDR
|
||
[[ -n $PUBLIC_HOST ]] || die "没有可用于 IPv6 监听的全局地址"
|
||
return 0
|
||
fi
|
||
PUBLIC_HOST=$(oci_public_ip || true)
|
||
[[ -n $PUBLIC_HOST ]] && return 0
|
||
PUBLIC_HOST=$(curl -4 -fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)
|
||
[[ -n $PUBLIC_HOST ]] && return 0
|
||
die "无法自动检测公网 IPv4,请使用 --host 指定,或改用 --listen ::"
|
||
}
|
||
|
||
|
||
discover_local_addresses() {
|
||
mapfile -t LOCAL_ADDRESSES < <(
|
||
ip -o addr show up scope global |
|
||
awk '$2 !~ /^(docker|br-|veth|virbr|tailscale|wg)/ {sub(/\/.*/, "", $4); print $4}' |
|
||
sort -u
|
||
)
|
||
((${#LOCAL_ADDRESSES[@]} > 0)) || die "没有检测到全局 IPv4/IPv6 地址"
|
||
}
|
||
|
||
|
||
address_is_local() {
|
||
local target=$1 address
|
||
for address in "${LOCAL_ADDRESSES[@]}"; do
|
||
[[ $address == "$target" ]] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
|
||
validate_bind_addresses() {
|
||
local address
|
||
for address in "${BIND_ADDRESSES[@]}"; do
|
||
valid_ip "$address" || die "无效的 --bind 地址: $address"
|
||
address_is_local "$address" || die "--bind 地址不在本机: $address"
|
||
done
|
||
local size=${#BIND_ADDRESSES[@]}
|
||
((size == 0 || size == 1 || size == COUNT)) || die "--bind 数量必须是 1 或代理数量 $COUNT"
|
||
}
|
||
|
||
|
||
interface_for_address() {
|
||
local target=$1
|
||
ip -o addr show | awk -v target="$target" '
|
||
{address=$4; sub(/\/.*/, "", address)}
|
||
address == target {interface=$2; sub(/@.*/, "", interface); print interface; exit}
|
||
'
|
||
}
|
||
|
||
|
||
route_for_address() {
|
||
local address=$1
|
||
if [[ $address == *:* ]]; then
|
||
ip -6 route get 2606:4700:4700::1111 from "$address"
|
||
else
|
||
ip -4 route get 1.1.1.1 from "$address"
|
||
fi
|
||
}
|
||
|
||
|
||
validate_bind_routes() {
|
||
local address expected route actual
|
||
for address in "${RESOLVED_BINDS[@]}"; do
|
||
[[ -n $address ]] || continue
|
||
expected=$(interface_for_address "$address")
|
||
route=$(route_for_address "$address" 2>/dev/null) || die "$address 没有可用的出站路由"
|
||
actual=$(awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}' <<<"$route")
|
||
[[ -n $expected && $actual == "$expected" ]] ||
|
||
die "$address 应从 $expected 出口,内核实际选择 ${actual:-未知接口};请先修复策略路由"
|
||
done
|
||
}
|
||
|
||
|
||
prepare_binds() {
|
||
local index size=${#BIND_ADDRESSES[@]}
|
||
if ((size == 0 && AUTO_BIND == 0)); then
|
||
for ((index = 0; index < COUNT; index++)); do RESOLVED_BINDS+=(""); done
|
||
return 0
|
||
fi
|
||
discover_local_addresses
|
||
validate_bind_addresses
|
||
for ((index = 0; index < COUNT; index++)); do
|
||
if ((size == COUNT)); then
|
||
RESOLVED_BINDS+=("${BIND_ADDRESSES[index]}")
|
||
elif ((size == 1)); then
|
||
RESOLVED_BINDS+=("${BIND_ADDRESSES[0]}")
|
||
elif ((AUTO_BIND == 1)); then
|
||
RESOLVED_BINDS+=("${LOCAL_ADDRESSES[index % ${#LOCAL_ADDRESSES[@]}]}")
|
||
else
|
||
RESOLVED_BINDS+=("")
|
||
fi
|
||
done
|
||
}
|
||
|
||
|
||
prepare_credentials() {
|
||
local index
|
||
for ((index = 0; index < COUNT; index++)); do
|
||
if ((NO_AUTH == 1)); then
|
||
USERS+=(""); PASSWORDS+=("")
|
||
elif [[ -n $COMMON_USER ]]; then
|
||
USERS+=("$COMMON_USER"); PASSWORDS+=("$COMMON_PASS")
|
||
else
|
||
USERS+=("proxy$(openssl rand -hex 3)")
|
||
PASSWORDS+=("$(openssl rand -hex 16)")
|
||
fi
|
||
done
|
||
}
|
||
|
||
|
||
prepare_ports() {
|
||
local index
|
||
for ((index = 0; index < COUNT; index++)); do
|
||
PORTS+=("$((START_PORT + index))")
|
||
done
|
||
}
|
||
|
||
|
||
port_in_use() {
|
||
local port=$1
|
||
ss -H -ltn | awk -v suffix=":$port" '$4 ~ suffix "$" {found=1} END {exit !found}'
|
||
}
|
||
|
||
|
||
validate_ports() {
|
||
local port config
|
||
for port in "${PORTS[@]}"; do
|
||
config="$CONF_DIR/$port.conf"
|
||
if [[ -e $config && $REPLACE -eq 0 ]]; then
|
||
die "$config 已存在;使用 --replace 才能覆盖"
|
||
fi
|
||
if [[ ! -e $config ]] && port_in_use "$port"; then
|
||
die "端口 $port 已被其他进程占用"
|
||
fi
|
||
done
|
||
}
|
||
|
||
|
||
format_url_host() {
|
||
local host=${1#[}
|
||
host=${host%]}
|
||
[[ $host == *:* ]] && printf '[%s]' "$host" || printf '%s' "$host"
|
||
}
|
||
|
||
|
||
format_link() {
|
||
local user=$1 pass=$2 host port=$4
|
||
host=$(format_url_host "$3")
|
||
if [[ -n $user ]]; then
|
||
printf 'socks5://%s:%s@%s:%s\n' "$user" "$pass" "$host" "$port"
|
||
else
|
||
printf 'socks5://%s:%s\n' "$host" "$port"
|
||
fi
|
||
}
|
||
|
||
|
||
print_links() {
|
||
local index
|
||
for ((index = 0; index < COUNT; index++)); do
|
||
format_link "${USERS[index]}" "${PASSWORDS[index]}" "$PUBLIC_HOST" "${PORTS[index]}"
|
||
done
|
||
}
|
||
|
||
|
||
show_plan() {
|
||
local index bind auth
|
||
log "计划创建 $COUNT 个实例,监听地址 $LISTEN_ADDR"
|
||
for ((index = 0; index < COUNT; index++)); do
|
||
bind=${RESOLVED_BINDS[index]:-自动}
|
||
auth=${USERS[index]:-无认证}
|
||
log "端口 ${PORTS[index]};出口 ${bind};用户 $auth"
|
||
done
|
||
log "来源限制: ${ALLOW_LIST:-不限制}"
|
||
log "链接主机: $PUBLIC_HOST"
|
||
}
|
||
|
||
|
||
confirm_plan() {
|
||
[[ -t 0 && $NON_INTERACTIVE -eq 0 ]] || return 0
|
||
local answer
|
||
read -r -p "继续安装并启动服务?[Y/n]: " answer
|
||
[[ ! $answer =~ ^[Nn]$ ]] || exit 0
|
||
}
|
||
|
||
|
||
bind_supported() {
|
||
local usage
|
||
usage=$({ "$MICROSOCKS_BIN" -h || true; } 2>&1 | grep -m1 '^usage:' || true)
|
||
# microsocks 1.0.1 lists a bare "-b" flag; only "-b bindaddr" accepts an address.
|
||
[[ $usage =~ [[:space:]]-b[[:space:]]+[[:alnum:]] ]]
|
||
}
|
||
|
||
|
||
check_bind_support() {
|
||
local has_bind=0 address
|
||
for address in "${RESOLVED_BINDS[@]}"; do
|
||
[[ -n $address ]] && has_bind=1
|
||
done
|
||
((has_bind == 0)) && return 0
|
||
bind_supported && return 0
|
||
log "$MICROSOCKS_BIN 的 -b 不能指定出站地址,改用上游源码编译"
|
||
install_from_source
|
||
bind_supported || die "当前 microsocks 不支持 -b 出站绑定"
|
||
}
|
||
|
||
|
||
write_unit_header() {
|
||
cat >"$1" <<EOF
|
||
[Unit]
|
||
Description=MicroSocks SOCKS5 Proxy on port %i
|
||
Wants=network-online.target
|
||
After=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
EnvironmentFile=$CONF_DIR/%i.conf
|
||
ExecStart=$MICROSOCKS_BIN -i \${LISTEN_ADDR} -p %i \$AUTH_OPTIONS \$BIND_OPTIONS
|
||
Restart=on-failure
|
||
RestartSec=2
|
||
User=nobody
|
||
Group=nogroup
|
||
NoNewPrivileges=true
|
||
EOF
|
||
}
|
||
|
||
|
||
write_unit_sandbox() {
|
||
cat >>"$1" <<'EOF'
|
||
PrivateTmp=true
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ProtectKernelTunables=true
|
||
ProtectKernelModules=true
|
||
ProtectControlGroups=true
|
||
RestrictAddressFamilies=AF_INET AF_INET6
|
||
LockPersonality=true
|
||
MemoryDenyWriteExecute=true
|
||
CapabilityBoundingSet=
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
}
|
||
|
||
|
||
write_unit_file() {
|
||
local temporary="$WORK_DIR/microsocks@.service"
|
||
write_unit_header "$temporary"
|
||
write_unit_sandbox "$temporary"
|
||
install -m 0644 "$temporary" "$UNIT_FILE"
|
||
}
|
||
|
||
|
||
render_instance_config() {
|
||
local auth_options="" bind_options=""
|
||
[[ -z $INST_USER ]] || auth_options="-u $INST_USER -P $INST_PASS"
|
||
[[ -z $INST_BIND ]] || bind_options="-b $INST_BIND"
|
||
printf 'LISTEN_ADDR=%s\n' "$INST_LISTEN"
|
||
printf 'SOCKS_USER=%s\n' "$INST_USER"
|
||
printf 'SOCKS_PASS=%s\n' "$INST_PASS"
|
||
printf 'AUTH_OPTIONS="%s"\n' "$auth_options"
|
||
printf 'BIND_OPTIONS="%s"\n' "$bind_options"
|
||
printf 'PUBLIC_HOST=%s\n' "$INST_HOST"
|
||
printf 'ALLOW_CIDRS="%s"\n' "$INST_ALLOW"
|
||
printf 'FIREWALL_OPENED=%s\n' "$INST_FIREWALL"
|
||
}
|
||
|
||
|
||
save_instance() {
|
||
local temporary="$WORK_DIR/$1.conf"
|
||
render_instance_config >"$temporary"
|
||
install -m 0600 "$temporary" "$CONF_DIR/$1.conf"
|
||
}
|
||
|
||
|
||
write_instance_config() {
|
||
local index=$1
|
||
INST_LISTEN=$LISTEN_ADDR
|
||
INST_USER=${USERS[index]}
|
||
INST_PASS=${PASSWORDS[index]}
|
||
INST_BIND=${RESOLVED_BINDS[index]}
|
||
INST_HOST=$PUBLIC_HOST
|
||
INST_ALLOW=$ALLOW_LIST
|
||
INST_FIREWALL=$OPEN_FIREWALL
|
||
save_instance "${PORTS[index]}"
|
||
}
|
||
|
||
|
||
install_configuration() {
|
||
install -d -m 0750 "$CONF_DIR"
|
||
write_unit_file
|
||
local index
|
||
for ((index = 0; index < COUNT; index++)); do
|
||
write_instance_config "$index"
|
||
done
|
||
systemctl daemon-reload
|
||
}
|
||
|
||
|
||
ensure_active() {
|
||
systemctl is-active --quiet "$1" && return 0
|
||
journalctl -u "$1" -n 30 --no-pager >&2 || true
|
||
die "$1 启动失败"
|
||
}
|
||
|
||
|
||
start_services() {
|
||
local port unit
|
||
for port in "${PORTS[@]}"; do
|
||
unit="microsocks@$port.service"
|
||
if systemctl is-active --quiet "$unit"; then
|
||
systemctl restart "$unit" || true
|
||
else
|
||
systemctl enable --now "$unit" || true
|
||
fi
|
||
ensure_active "$unit"
|
||
done
|
||
}
|
||
|
||
|
||
ufw_active() {
|
||
command -v ufw >/dev/null && ufw status | head -n 1 | grep -qw active
|
||
}
|
||
|
||
|
||
firewalld_active() {
|
||
command -v firewall-cmd >/dev/null && systemctl is-active --quiet firewalld
|
||
}
|
||
|
||
|
||
open_detected_firewall() {
|
||
[[ $OPEN_FIREWALL -eq 1 ]] || return 0
|
||
local port
|
||
if ufw_active; then
|
||
for port in "${PORTS[@]}"; do ufw allow "$port/tcp" >&2; done
|
||
fi
|
||
if firewalld_active; then
|
||
for port in "${PORTS[@]}"; do firewall-cmd --permanent --add-port="$port/tcp" >&2; done
|
||
firewall-cmd --reload >&2
|
||
fi
|
||
}
|
||
|
||
|
||
close_detected_firewall() {
|
||
(($# > 0)) || return 0
|
||
local port
|
||
if ufw_active; then
|
||
for port in "$@"; do ufw delete allow "$port/tcp" >&2 || log "UFW 未能删除 $port/tcp"; done
|
||
fi
|
||
if firewalld_active; then
|
||
for port in "$@"; do
|
||
firewall-cmd --permanent --remove-port="$port/tcp" >&2 || log "firewalld 未能删除 $port/tcp"
|
||
done
|
||
firewall-cmd --reload >&2
|
||
fi
|
||
}
|
||
|
||
|
||
render_acl_rules() {
|
||
local port cidr v4 v6
|
||
local -a ports=() cidrs=()
|
||
mapfile -t ports < <(instance_ports)
|
||
for port in "${ports[@]}"; do
|
||
load_instance "$port"
|
||
[[ -n $INST_ALLOW ]] || continue
|
||
read -r -a cidrs <<<"$INST_ALLOW"
|
||
v4="" v6=""
|
||
for cidr in "${cidrs[@]}"; do
|
||
if [[ $cidr == *:* ]]; then v6+=${v6:+, }$cidr; else v4+=${v4:+, }$cidr; fi
|
||
done
|
||
[[ -z $v4 ]] || printf '\t\ttcp dport %s ip saddr { %s } accept\n' "$port" "$v4"
|
||
[[ -z $v6 ]] || printf '\t\ttcp dport %s ip6 saddr { %s } accept\n' "$port" "$v6"
|
||
printf '\t\ttcp dport %s drop\n' "$port"
|
||
done
|
||
}
|
||
|
||
|
||
write_acl_file() {
|
||
local temporary="$WORK_DIR/acl.nft"
|
||
{
|
||
printf 'table inet microsocks\ndelete table inet microsocks\n'
|
||
printf 'table inet microsocks {\n\tchain input {\n'
|
||
printf '\t\ttype filter hook input priority 0; policy accept;\n'
|
||
printf '\t\tiif "lo" accept\n%s\n\t}\n}\n' "$1"
|
||
} >"$temporary"
|
||
nft -c -f "$temporary" || die "nftables 规则校验失败"
|
||
install -m 0600 "$temporary" "$ACL_FILE"
|
||
}
|
||
|
||
|
||
# Reload the allow list before every start so it survives reboots and fails closed.
|
||
install_acl_hook() {
|
||
local dropin="$UNIT_FILE.d/10-acl.conf" temporary="$WORK_DIR/10-acl.conf"
|
||
cat >"$temporary" <<EOF
|
||
[Service]
|
||
ExecStartPre=+/bin/sh -c 'test ! -e $ACL_FILE || exec nft -f $ACL_FILE'
|
||
EOF
|
||
cmp -s "$temporary" "$dropin" && return 0
|
||
install -d -m 0755 "$UNIT_FILE.d"
|
||
install -m 0644 "$temporary" "$dropin"
|
||
systemctl daemon-reload
|
||
}
|
||
|
||
|
||
ensure_nft() {
|
||
command -v nft >/dev/null && return 0
|
||
apt_update_once
|
||
DEBIAN_FRONTEND=noninteractive apt-get install -y nftables >&2
|
||
}
|
||
|
||
|
||
acl_table_loaded() {
|
||
command -v nft >/dev/null && nft list table inet microsocks >/dev/null 2>&1
|
||
}
|
||
|
||
|
||
apply_acl() {
|
||
local rules
|
||
rules=$(render_acl_rules)
|
||
if [[ -z $rules ]]; then
|
||
rm -f -- "$ACL_FILE"
|
||
if acl_table_loaded; then
|
||
nft delete table inet microsocks
|
||
log "已移除来源限制规则"
|
||
fi
|
||
return 0
|
||
fi
|
||
ensure_nft
|
||
write_acl_file "$rules"
|
||
install_acl_hook
|
||
nft -f "$ACL_FILE"
|
||
log "来源限制已生效(nftables 表 inet microsocks)"
|
||
}
|
||
|
||
|
||
instance_ports() {
|
||
local conf name
|
||
for conf in "$CONF_DIR"/*.conf; do
|
||
name=${conf##*/}
|
||
name=${name%.conf}
|
||
if [[ -f $conf ]] && is_uint "$name"; then
|
||
printf '%s\n' "$name"
|
||
fi
|
||
done | sort -n
|
||
}
|
||
|
||
|
||
load_instance() {
|
||
local line key value
|
||
INST_LISTEN="" INST_USER="" INST_PASS="" INST_BIND=""
|
||
INST_HOST="" INST_ALLOW="" INST_FIREWALL=0
|
||
while IFS= read -r line || [[ -n $line ]]; do
|
||
key=${line%%=*}
|
||
value=${line#*=}
|
||
value=${value#\"}
|
||
value=${value%\"}
|
||
case "$key" in
|
||
LISTEN_ADDR) INST_LISTEN=$value ;;
|
||
SOCKS_USER) INST_USER=$value ;;
|
||
SOCKS_PASS) INST_PASS=$value ;;
|
||
BIND_OPTIONS) INST_BIND=${value#-b } ;;
|
||
PUBLIC_HOST) INST_HOST=$value ;;
|
||
ALLOW_CIDRS) INST_ALLOW=$value ;;
|
||
FIREWALL_OPENED) INST_FIREWALL=$value ;;
|
||
esac
|
||
done <"$CONF_DIR/$1.conf"
|
||
}
|
||
|
||
|
||
resolve_targets() {
|
||
local port
|
||
local -a existing=()
|
||
mapfile -t existing < <(instance_ports)
|
||
if ((${#existing[@]} == 0)); then
|
||
[[ $ACTION == list || $ACTION == links ]] || die "没有找到 microsocks 实例"
|
||
log "没有找到 microsocks 实例"
|
||
exit 0
|
||
fi
|
||
((${#TARGET_PORTS[@]} > 0)) || TARGET_PORTS=("${existing[@]}")
|
||
mapfile -t TARGET_PORTS < <(printf '%s\n' "${TARGET_PORTS[@]}" | sort -nu)
|
||
for port in "${TARGET_PORTS[@]}"; do
|
||
[[ -f $CONF_DIR/$port.conf ]] || die "实例 $port 不存在"
|
||
done
|
||
}
|
||
|
||
|
||
print_row() {
|
||
printf '%-6s %-10s %-16s %-16s %-14s %s\n' "$@"
|
||
}
|
||
|
||
|
||
list_instances() {
|
||
local port state allow
|
||
print_row PORT STATE LISTEN EGRESS USER ALLOW
|
||
for port in "${TARGET_PORTS[@]}"; do
|
||
load_instance "$port"
|
||
state=$(systemctl is-active "microsocks@$port.service" 2>/dev/null || true)
|
||
allow=${INST_ALLOW// /,}
|
||
print_row "$port" "${state:-unknown}" "$INST_LISTEN" "${INST_BIND:-default}" \
|
||
"${INST_USER:--}" "${allow:-any}"
|
||
done
|
||
}
|
||
|
||
|
||
print_instance_links() {
|
||
local port
|
||
for port in "${TARGET_PORTS[@]}"; do
|
||
load_instance "$port"
|
||
format_link "$INST_USER" "$INST_PASS" "${PUBLIC_HOST:-$INST_HOST}" "$port"
|
||
done
|
||
}
|
||
|
||
|
||
update_instances() {
|
||
local port
|
||
for port in "${TARGET_PORTS[@]}"; do
|
||
load_instance "$port"
|
||
if ((${#ALLOW_ARGS[@]} > 0 || ALLOW_ALL == 1)); then
|
||
INST_ALLOW=$ALLOW_LIST
|
||
fi
|
||
if [[ -n $PUBLIC_HOST ]]; then
|
||
INST_HOST=$PUBLIC_HOST
|
||
fi
|
||
save_instance "$port"
|
||
log "已更新 ${port}:来源限制 ${INST_ALLOW:-不限制};链接主机 $INST_HOST"
|
||
done
|
||
apply_acl
|
||
print_instance_links
|
||
}
|
||
|
||
|
||
control_instances() {
|
||
local port unit
|
||
for port in "${TARGET_PORTS[@]}"; do
|
||
unit="microsocks@$port.service"
|
||
case "$ACTION" in
|
||
start) systemctl enable --now "$unit" || true ;;
|
||
stop) systemctl disable --now "$unit" ;;
|
||
restart) systemctl restart "$unit" || true ;;
|
||
esac
|
||
[[ $ACTION == stop ]] || ensure_active "$unit"
|
||
log "$unit: $(systemctl is-active "$unit" || true)"
|
||
done
|
||
}
|
||
|
||
|
||
confirm_remove() {
|
||
[[ -t 0 && $NON_INTERACTIVE -eq 0 ]] || return 0
|
||
local answer
|
||
read -r -p "确认删除实例 ${TARGET_PORTS[*]}?[y/N]: " answer
|
||
[[ $answer =~ ^[Yy]$ ]] || exit 0
|
||
}
|
||
|
||
|
||
remove_instances() {
|
||
local port
|
||
local -a opened=()
|
||
for port in "${TARGET_PORTS[@]}"; do
|
||
load_instance "$port"
|
||
[[ $INST_FIREWALL != 1 ]] || opened+=("$port")
|
||
systemctl disable --now "microsocks@$port.service" || log "停止 microsocks@$port 失败"
|
||
rm -f -- "$CONF_DIR/$port.conf"
|
||
log "已删除 microsocks@$port"
|
||
done
|
||
apply_acl
|
||
close_detected_firewall "${opened[@]}"
|
||
}
|
||
|
||
|
||
acquire_lock() {
|
||
exec 9>/run/lock/microsocks-setup.lock
|
||
flock -n 9 || die "另一个 microsocks 任务正在运行"
|
||
}
|
||
|
||
|
||
cleanup() {
|
||
[[ -n ${WORK_DIR:-} && -d ${WORK_DIR:-} ]] || return 0
|
||
[[ $WORK_DIR == /tmp/microsocks-setup.* ]] || return 0
|
||
rm -rf -- "$WORK_DIR"
|
||
}
|
||
|
||
|
||
prepare_plan() {
|
||
detect_public_host
|
||
prepare_binds
|
||
validate_bind_routes
|
||
prepare_ports
|
||
prepare_credentials
|
||
validate_ports
|
||
show_plan
|
||
}
|
||
|
||
|
||
run_create() {
|
||
interactive_options
|
||
detect_os
|
||
[[ $DRY_RUN -eq 1 || $EUID -eq 0 ]] || die "请使用 root 或 sudo 执行"
|
||
ensure_base_tools
|
||
validate_options
|
||
validate_auth_mode
|
||
prepare_plan
|
||
if [[ $DRY_RUN -eq 1 ]]; then
|
||
log "dry-run:未修改系统;以下链接尚未生效"
|
||
print_links
|
||
return 0
|
||
fi
|
||
confirm_plan
|
||
acquire_lock
|
||
install_microsocks
|
||
check_bind_support
|
||
install_configuration
|
||
apply_acl
|
||
start_services
|
||
open_detected_firewall
|
||
log "请同时在云平台安全组/安全列表中放行对应 TCP 端口"
|
||
print_links
|
||
}
|
||
|
||
|
||
run_manage() {
|
||
[[ $EUID -eq 0 ]] || die "请使用 root 或 sudo 执行"
|
||
command -v systemctl >/dev/null || die "未检测到 systemd"
|
||
validate_manage_options
|
||
resolve_targets
|
||
case "$ACTION" in
|
||
list) list_instances ;;
|
||
links) print_instance_links ;;
|
||
update) acquire_lock; update_instances ;;
|
||
start|stop|restart) acquire_lock; control_instances ;;
|
||
remove) confirm_remove; acquire_lock; remove_instances ;;
|
||
esac
|
||
}
|
||
|
||
|
||
main() {
|
||
parse_cli "$@"
|
||
if [[ $ACTION == create ]]; then
|
||
run_create
|
||
else
|
||
run_manage
|
||
fi
|
||
}
|
||
|
||
|
||
trap cleanup EXIT
|
||
WORK_DIR=$(mktemp -d /tmp/microsocks-setup.XXXXXX)
|
||
|
||
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
|
||
main "$@"
|
||
fi
|