Files
tools/linux/setup-microsocks.sh
T

604 lines
17 KiB
Bash
Executable File

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