#!/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" <>"$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" </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