diff --git a/linux/setup-microsocks.sh b/linux/setup-microsocks.sh new file mode 100755 index 0000000..9390828 --- /dev/null +++ b/linux/setup-microsocks.sh @@ -0,0 +1,603 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +# Debian/Ubuntu MicroSocks multi-instance installer. +# Status messages go to stderr; created socks5:// links go to stdout. + +COUNT=0 +COUNT_SET=0 +START_PORT=1080 +START_PORT_SET=0 +LISTEN_ADDR="0.0.0.0" +PUBLIC_HOST="" +COMMON_USER="" +COMMON_PASS="" +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" +UNIT_FILE="/etc/systemd/system/microsocks@.service" + +declare -a BIND_ADDRESSES=() +declare -a RESOLVED_BINDS=() +declare -a PORTS=() +declare -a USERS=() +declare -a PASSWORDS=() +declare -a LOCAL_ADDRESSES=() + + +log() { + printf '[microsocks] %s\n' "$*" >&2 +} + + +die() { + printf '[microsocks] 错误: %s\n' "$*" >&2 + exit 1 +} + + +usage() { + cat <<'EOF' +用法:sudo ./setup-microsocks.sh [选项] + + -n, --count N 创建 N 个代理;多个 --bind 时可省略 + --start-port PORT 起始端口,默认 1080 + --listen IP 监听地址,默认 0.0.0.0 + --host HOST 返回链接使用的公网 IP 或域名 + --bind IP 绑定出站源地址;可重复指定 + --auto-bind 自动轮流使用本机所有全局 IP 作为出口 + --user USER 所有实例使用同一用户名 + --password PASS 所有实例使用同一密码 + --no-auth 仅允许在回环监听地址上关闭认证 + --replace 覆盖相同端口的既有 microsocks 实例 + --open-firewall 自动放行已启用的 UFW/firewalld 端口 + --non-interactive 不询问确认 + --dry-run 仅显示计划和链接,不修改系统 + -h, --help 显示帮助 + +示例: + 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 +EOF +} + + +need_value() { + [[ $# -ge 2 && -n ${2:-} ]] || die "$1 缺少参数" +} + + +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 ;; + --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 ;; + --dry-run) DRY_RUN=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "未知参数: $1" ;; + 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 + 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 + local answer + read -r -p "是否按检测到的本机 IP 自动分配出口?[y/N]: " answer + [[ $answer =~ ^[Yy]$ ]] && AUTO_BIND=1 + 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])?$ ]] +} + + +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 地址" +} + + +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 +} + + +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 + 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 +} + + +install_from_source() { + apt_update_once + DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential ca-certificates + log "从上游源码编译 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 +} + + +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; then + MICROSOCKS_BIN=$(command -v microsocks) + return 0 + fi + install_from_source + MICROSOCKS_BIN=/usr/bin/microsocks +} + + +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" +} + + +print_links() { + local index host + host=$(format_url_host "$PUBLIC_HOST") + 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 + 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 "链接主机: $PUBLIC_HOST" +} + + +confirm_plan() { + [[ -t 0 && $NON_INTERACTIVE -eq 0 ]] || return 0 + local answer + read -r -p "继续安装并启动服务?[Y/n]: " answer + [[ ! $answer =~ ^[Nn]$ ]] || exit 0 +} + + +check_bind_support() { + local has_bind=0 address help + 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 出站绑定" +} + + +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" +} + + +write_instance_config() { + local index=$1 port=${PORTS[$1]} temporary="$WORK_DIR/${PORTS[$1]}.conf" + 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" +} + + +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 +} + + +start_services() { + local port + for port in "${PORTS[@]}"; do + if systemctl is-active --quiet "microsocks@$port.service"; then + systemctl restart "microsocks@$port.service" + 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 启动失败" + fi + done +} + + +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 + 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 + fi + ((firewalld == 0)) || firewall-cmd --reload +} + + +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 +} + + +main() { + parse_args "$@" + 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 + exec 9>/run/lock/microsocks-setup.lock + flock -n 9 || die "另一个安装任务正在运行" + install_microsocks + check_bind_support + install_configuration + start_services + open_detected_firewall + log "请同时在云平台安全组/安全列表中放行对应 TCP 端口" + print_links +} + + +trap cleanup EXIT +WORK_DIR=$(mktemp -d /tmp/microsocks-setup.XXXXXX) + +if [[ ${BASH_SOURCE[0]} == "$0" ]]; then + main "$@" +fi