diff --git a/linux/setup_microsocks.sh b/linux/setup_microsocks.sh index 9390828..6afd680 100755 --- a/linux/setup_microsocks.sh +++ b/linux/setup_microsocks.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash set -Eeuo pipefail -# Debian/Ubuntu MicroSocks multi-instance installer. -# Status messages go to stderr; created socks5:// links go to stdout. +# 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 @@ -12,6 +13,8 @@ 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 @@ -22,7 +25,17 @@ 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=() @@ -30,6 +43,8 @@ declare -a PORTS=() declare -a USERS=() declare -a PASSWORDS=() declare -a LOCAL_ADDRESSES=() +declare -a ALLOW_ARGS=() +declare -a TARGET_PORTS=() log() { @@ -45,27 +60,49 @@ die() { usage() { cat <<'EOF' -用法:sudo ./setup-microsocks.sh [选项] +用法: + 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 端口 - --non-interactive 不询问确认 + -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 + 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 } @@ -75,6 +112,18 @@ need_value() { } +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 @@ -83,13 +132,14 @@ parse_args() { --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 ;; - --non-interactive) NON_INTERACTIVE=1; shift ;; + -y|--non-interactive) NON_INTERACTIVE=1; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) usage; exit 0 ;; *) die "未知参数: $1" ;; @@ -98,6 +148,21 @@ parse_args() { } +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 @@ -107,6 +172,7 @@ prompt_number() { 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 @@ -115,9 +181,12 @@ interactive_options() { START_PORT=$(prompt_number "起始端口" 1080) fi if is_uint "$COUNT" && ((COUNT > 1 && ${#BIND_ADDRESSES[@]} == 0)); then - local answer read -r -p "是否按检测到的本机 IP 自动分配出口?[y/N]: " answer - [[ $answer =~ ^[Yy]$ ]] && AUTO_BIND=1 + 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 } @@ -149,6 +218,39 @@ valid_host() { } +# 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)) @@ -162,6 +264,7 @@ validate_options() { [[ -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 } @@ -175,6 +278,26 @@ validate_auth_mode() { } +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 @@ -190,7 +313,7 @@ detect_os() { apt_update_once() { [[ $APT_UPDATED -eq 1 ]] && return 0 log "更新 APT 索引" - DEBIAN_FRONTEND=noninteractive apt-get update + DEBIAN_FRONTEND=noninteractive apt-get update >&2 APT_UPDATED=1 } @@ -203,17 +326,19 @@ ensure_base_tools() { ((${#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 + 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 + 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" - install -m 0755 "$WORK_DIR/microsocks/microsocks" /usr/bin/microsocks + make -C "$WORK_DIR/microsocks" >&2 + install -m 0755 "$WORK_DIR/microsocks/microsocks" "$SOURCE_BIN" + MICROSOCKS_BIN=$SOURCE_BIN } @@ -224,12 +349,11 @@ install_microsocks() { fi apt_update_once log "尝试从 APT 安装 microsocks" - if DEBIAN_FRONTEND=noninteractive apt-get install -y microsocks; then + if DEBIAN_FRONTEND=noninteractive apt-get install -y microsocks >&2; then MICROSOCKS_BIN=$(command -v microsocks) return 0 fi install_from_source - MICROSOCKS_BIN=/usr/bin/microsocks } @@ -401,15 +525,21 @@ format_url_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 host - host=$(format_url_host "$PUBLIC_HOST") + local index for ((index = 0; index < COUNT; index++)); do - if [[ -n ${USERS[index]} ]]; then - printf 'socks5://%s:%s@%s:%s\n' "${USERS[index]}" "${PASSWORDS[index]}" "$host" "${PORTS[index]}" - else - printf 'socks5://%s:%s\n' "$host" "${PORTS[index]}" - fi + format_link "${USERS[index]}" "${PASSWORDS[index]}" "$PUBLIC_HOST" "${PORTS[index]}" done } @@ -420,8 +550,9 @@ show_plan() { for ((index = 0; index < COUNT; index++)); do bind=${RESOLVED_BINDS[index]:-自动} auth=${USERS[index]:-无认证} - log "端口 ${PORTS[index]};出口 $bind;用户 $auth" + log "端口 ${PORTS[index]};出口 ${bind};用户 $auth" done + log "来源限制: ${ALLOW_LIST:-不限制}" log "链接主机: $PUBLIC_HOST" } @@ -434,14 +565,24 @@ confirm_plan() { } +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 help + local has_bind=0 address for address in "${RESOLVED_BINDS[@]}"; do [[ -n $address ]] && has_bind=1 done ((has_bind == 0)) && return 0 - help=$({ "$MICROSOCKS_BIN" -h || true; } 2>&1) - grep -Eq -- '(^|[[:space:]])-b([[:space:]]|$)' <<<"$help" || die "当前 microsocks 不支持 -b 出站绑定" + bind_supported && return 0 + log "$MICROSOCKS_BIN 的 -b 不能指定出站地址,改用上游源码编译" + install_from_source + bind_supported || die "当前 microsocks 不支持 -b 出站绑定" } @@ -492,20 +633,38 @@ write_unit_file() { } -write_instance_config() { - local index=$1 port=${PORTS[$1]} temporary="$WORK_DIR/${PORTS[$1]}.conf" +render_instance_config() { local auth_options="" bind_options="" - [[ -z ${USERS[index]} ]] || auth_options="-u ${USERS[index]} -P ${PASSWORDS[index]}" - [[ -z ${RESOLVED_BINDS[index]} ]] || bind_options="-b ${RESOLVED_BINDS[index]}" - { - printf 'LISTEN_ADDR=%s\n' "$LISTEN_ADDR" - printf 'SOCKS_USER=%s\n' "${USERS[index]}" - printf 'SOCKS_PASS=%s\n' "${PASSWORDS[index]}" - printf 'AUTH_OPTIONS="%s"\n' "$auth_options" - printf 'BIND_OPTIONS="%s"\n' "$bind_options" - printf 'PUBLIC_HOST=%s\n' "$PUBLIC_HOST" - } >"$temporary" - install -m 0600 "$temporary" "$CONF_DIR/$port.conf" + [[ -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]}" } @@ -520,33 +679,279 @@ install_configuration() { } +ensure_active() { + systemctl is-active --quiet "$1" && return 0 + journalctl -u "$1" -n 30 --no-pager >&2 || true + die "$1 启动失败" +} + + start_services() { - local port + local port unit for port in "${PORTS[@]}"; do - if systemctl is-active --quiet "microsocks@$port.service"; then - systemctl restart "microsocks@$port.service" + unit="microsocks@$port.service" + if systemctl is-active --quiet "$unit"; then + systemctl restart "$unit" || true else - systemctl enable --now "microsocks@$port.service" - fi - if ! systemctl is-active --quiet "microsocks@$port.service"; then - journalctl -u "microsocks@$port.service" -n 30 --no-pager >&2 || true - die "microsocks@$port 启动失败" + 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 firewalld=0 - if command -v ufw >/dev/null && ufw status | head -n 1 | grep -qw active; then - for port in "${PORTS[@]}"; do ufw allow "$port/tcp"; done + local port + if ufw_active; then + for port in "${PORTS[@]}"; do ufw allow "$port/tcp" >&2; done fi - if command -v firewall-cmd >/dev/null && systemctl is-active --quiet firewalld; then - firewalld=1 - for port in "${PORTS[@]}"; do firewall-cmd --permanent --add-port="$port/tcp"; done + if firewalld_active; then + for port in "${PORTS[@]}"; do firewall-cmd --permanent --add-port="$port/tcp" >&2; done + firewall-cmd --reload >&2 fi - ((firewalld == 0)) || firewall-cmd --reload +} + + +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" </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 任务正在运行" } @@ -568,8 +973,7 @@ prepare_plan() { } -main() { - parse_args "$@" +run_create() { interactive_options detect_os [[ $DRY_RUN -eq 1 || $EUID -eq 0 ]] || die "请使用 root 或 sudo 执行" @@ -583,11 +987,11 @@ main() { return 0 fi confirm_plan - exec 9>/run/lock/microsocks-setup.lock - flock -n 9 || die "另一个安装任务正在运行" + acquire_lock install_microsocks check_bind_support install_configuration + apply_acl start_services open_detected_firewall log "请同时在云平台安全组/安全列表中放行对应 TCP 端口" @@ -595,6 +999,31 @@ main() { } +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)