Files
tools/linux/setup_microsocks.sh
T

1033 lines
29 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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