ssh 隧道管理工具
·
服务端有多个服务希望在本地访问,并且不想开放那么多端口,可以使用这个工具 。
trae 生成脚本 记录一下
#!/usr/bin/env bash
# ssh-tunnel.sh —— 纯 shell SSH 隧道管理工具
# 支持 local(-L) / remote(-R) 两种转发;交互式配置 CRUD;可选 autossh 自动重连
# 认证完全交给系统 ssh(密钥 / ~/.ssh/config / ssh-agent)
set -u
# ============== 全局配置 ==============
# 配置文件统一放在脚本所在目录下,便于随项目一起管理与备份
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
CONFIG_FILE="${SCRIPT_DIR}/config"
PID_DIR="${SCRIPT_DIR}/pids"
LOG_DIR="${SCRIPT_DIR}/logs"
# ssh 公共参数:-N 不执行远程命令;ExitOnForwardFailure 端口占用即退出便于排错
SSH_COMMON_OPTS="-N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=3"
# 字段顺序,用于序列化/解析
FIELDS="type ssh_host ssh_port ssh_user bind_host bind_port target_host target_port auto_start auto_reconnect"
# ============== 基础工具函数 ==============
# 初始化工作目录
init_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR"
[ -f "$CONFIG_FILE" ] || : > "$CONFIG_FILE"
}
# 统一日志输出(菜单场景下其实直接 echo,预留扩展)
log_info() { echo "[INFO] $*"; }
log_warn() { echo "[WARN] $*"; }
log_err() { echo "[ERROR] $*" >&2; }
# 检查某命令是否存在
has_cmd() { command -v "$1" >/dev/null 2>&1; }
# 端口是否监听(使用 bash 内置 /dev/tcp,零依赖)
# 注意:bash 编译时需启用 net redirection(macOS 自带 bash 3.2 默认支持)
port_listening() {
local host="$1" port="$2"
( exec 3<>"/dev/tcp/${host}/${port}" ) 2>/dev/null
}
# 校验:非空、端口为数字
valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }
valid_name() { [[ "$1" =~ ^[A-Za-z0-9_-]+$ ]] && [ -n "$1" ]; }
# ============== 配置文件读写 ==============
# 返回所有隧道名称(每行一个)
tunnels_list() {
awk '/^\[.*\]$/ { gsub(/[][]/,""); print }' "$CONFIG_FILE"
}
# 判断隧道是否存在
tunnel_exists() {
local name="$1"
[ -n "$name" ] && tunnels_list | grep -qx "$name"
}
# 读取某隧道某字段值;不存在返回空
tunnel_get_field() {
local name="$1" field="$2" in_section=0 line key val
while IFS= read -r line; do
case "$line" in
\[*\])
sec="${line#[}"
sec="${sec%]}"
if [ "$sec" = "$name" ]; then in_section=1
else [ "$in_section" = "1" ] && in_section=0
fi ;;
*)
if [ "$in_section" = "1" ]; then
key="${line%%=*}"
val="${line#*=}"
[ "$key" = "$field" ] && { printf '%s\n' "$val"; return 0; }
fi ;;
esac
done < "$CONFIG_FILE"
return 1
}
# 一次性把某隧道所有字段读到调用方指定的关联数组变量中
# 用法:tunnel_load <name> <assoc_var_name>
# 例如:tunnel_load web-dev cfg; echo "${cfg[bind_port]}"
# bash 3.2 不支持关联数组,这里改用普通变量名约定:<prefix>_<field>
# 用法:tunnel_load <name> <prefix> 之后用 ${<prefix>_type} ${<prefix>_bind_port} 等
tunnel_load() {
local name="$1" prefix="$2"
# 先把所有已知字段初始化为空,避免 set -u 下引用未定义变量
local f
for f in $FIELDS; do
eval "${prefix}_${f}=\"\""
done
local in_section=0 line key val
while IFS= read -r line; do
case "$line" in
\[*\])
sec="${line#[}"
sec="${sec%]}"
if [ "$sec" = "$name" ]; then in_section=1
else [ "$in_section" = "1" ] && in_section=0
fi ;;
*)
if [ "$in_section" = "1" ]; then
key="${line%%=*}"
val="${line#*=}"
eval "${prefix}_${key}=\"\$val\""
fi ;;
esac
done < "$CONFIG_FILE"
return 0
}
# 写入/更新一条隧道:参数依次为 name, type, ssh_host, ssh_port, ssh_user,
# bind_host, bind_port, target_host, target_port,
# auto_start, auto_reconnect
# 直接使用位置参数取值,避免空格拼接后被 cut 拆错
tunnel_save() {
local name="$1" type="$2" ssh_host="$3" ssh_port="$4" ssh_user="$5"
local bind_host="$6" bind_port="$7" target_host="$8" target_port="$9"
local auto_start="${10}" auto_reconnect="${11}"
local tmp
tmp=$(mktemp)
# 删除旧的同名段落
awk -v n="$name" '
/^\[.*\]$/ {
sec=$0; gsub(/[][]/,"",sec)
in_sec = (sec==n) ? 1 : 0
}
!in_sec { print }
' "$CONFIG_FILE" > "$tmp"
{
echo "[$name]"
printf 'type=%s\n' "$type"
printf 'ssh_host=%s\n' "$ssh_host"
printf 'ssh_port=%s\n' "$ssh_port"
printf 'ssh_user=%s\n' "$ssh_user"
printf 'bind_host=%s\n' "$bind_host"
printf 'bind_port=%s\n' "$bind_port"
printf 'target_host=%s\n' "$target_host"
printf 'target_port=%s\n' "$target_port"
printf 'auto_start=%s\n' "$auto_start"
printf 'auto_reconnect=%s\n' "$auto_reconnect"
echo ""
} >> "$tmp"
mv "$tmp" "$CONFIG_FILE"
}
# 删除一条隧道
tunnel_delete() {
local name="$1"
[ -z "$name" ] && return 1
local tmp
tmp=$(mktemp)
awk -v n="$name" '
/^\[.*\]$/ {
sec=$0; gsub(/[][]/,"",sec)
in_sec = (sec==n) ? 1 : 0
next
}
!in_sec { print }
' "$CONFIG_FILE" > "$tmp"
mv "$tmp" "$CONFIG_FILE"
}
# ============== 隧道进程控制 ==============
pid_file() { echo "$PID_DIR/$1.pid"; }
log_file() { echo "$LOG_DIR/$1.log"; }
# 读取 PID 文件中的 pid;为空或非数字返回空
read_pid() {
local f
f=$(pid_file "$1")
[ -f "$f" ] || return 1
local p
p=$(cat "$f" 2>/dev/null)
[[ "$p" =~ ^[0-9]+$ ]] || return 1
echo "$p"
}
# 进程是否存活
pid_alive() {
local p
p=$(read_pid "$1") || return 1
kill -0 "$p" >/dev/null 2>&1
}
# 构造 ssh 命令的转发参数部分
build_forward_opt() {
local name="$1"
# 一次性读取所有字段,避免多次 awk 扫文件
tunnel_load "$name" _bfo
case "${_bfo_type}" in
local) echo "-L ${_bfo_bind_host}:${_bfo_bind_port}:${_bfo_target_host}:${_bfo_target_port}" ;;
remote) echo "-R ${_bfo_bind_host}:${_bfo_bind_port}:${_bfo_target_host}:${_bfo_target_port}" ;;
*) return 1 ;;
esac
}
# 启动单条隧道
tunnel_start() {
local name="$1"
tunnel_exists "$name" || { log_err "隧道不存在: $name"; return 1; }
pid_alive "$name" && { log_info "隧道已在运行: $name"; return 0; }
# 一次性读取所有字段,避免多次扫配置文件
tunnel_load "$name" _st
local fwd_opt ssh_target port_opt
fwd_opt=$(build_forward_opt "$name") || { log_err "无法构造转发参数: $name"; return 1; }
# local 类型:启动前检查本地监听端口是否已被占用(避免 ssh 静默失败)
# 常见原因:本地已跑同端口服务(如 MySQL 3306),或上次隧道未清理干净
if [ "${_st_type}" = "local" ] && [ -n "${_st_bind_port}" ]; then
if port_listening "${_st_bind_host}" "${_st_bind_port}"; then
log_err "端口被占用: ${_st_bind_host}:${_st_bind_port}"
log_err "可能原因:1) 本地已跑该端口服务 2) 上次隧道残留 3) 其他进程占用"
log_err "排查命令:lsof -i :${_st_bind_port}"
return 1
fi
fi
# ssh 目标部分:user@host
if [ -n "${_st_ssh_user}" ]; then
ssh_target="${_st_ssh_user}@${_st_ssh_host}"
else
ssh_target="${_st_ssh_host}"
fi
port_opt=""
[ -n "${_st_ssh_port}" ] && [ "${_st_ssh_port}" != "22" ] && port_opt="-p ${_st_ssh_port}"
local lf pidf
lf=$(log_file "$name")
pidf=$(pid_file "$name")
: > "$lf" # 每次启动清空旧日志
rm -f "$pidf" # 清理旧 PID 文件,避免读到上次残留
# 优先使用 autossh(若安装)实现断线自动重连
# -M 0:关闭 autossh 的端口监控,改用 ssh 自带的 ServerAliveInterval 探活
if has_cmd autossh; then
# AUTOSSH_GATETIME=0 避免启动瞬时失败即放弃;AUTOSSH_PIDFILE 让 autossh 写 PID
AUTOSSH_GATETIME=0 AUTOSSH_LOGFILE="$lf" AUTOSSH_PIDFILE="$pidf" \
autossh -M 0 -f $SSH_COMMON_OPTS $port_opt $fwd_opt "$ssh_target" 2>>"$lf"
else
# -f 后台运行;-E 把日志写入文件
ssh -f -E "$lf" $SSH_COMMON_OPTS $port_opt $fwd_opt "$ssh_target" 2>>"$lf"
fi
# 轮询等待 PID 文件出现或进程可探测,最多等待 3 秒
# autossh 会写 AUTOSSH_PIDFILE;ssh -f 则用 pgrep 兜底
local waited=0 pid
while [ "$waited" -lt 30 ]; do
if [ -f "$pidf" ]; then
break
fi
pid=$(pgrep -f "ssh.*$fwd_opt.*$ssh_target" | head -n1)
[ -n "$pid" ] && { echo "$pid" > "$pidf"; break; }
sleep 0.1
waited=$((waited+1))
done
if pid_alive "$name"; then
log_info "已启动: ${name}"
else
log_err "启动失败: ${name}(查看日志: ${lf})"
return 1
fi
}
# 停止单条隧道
tunnel_stop() {
local name="$1"
if ! pid_alive "$name"; then
rm -f "$(pid_file "$name")"
log_info "隧道未运行: ${name}"
return 0
fi
local p
p=$(read_pid "$name")
# 优先 TERM,等 0.5s 仍存活则 KILL
kill "$p" 2>/dev/null
local i=0
while [ "$i" -lt 10 ] && kill -0 "$p" 2>/dev/null; do
sleep 0.1
i=$((i+1))
done
kill -0 "$p" 2>/dev/null && kill -9 "$p" 2>/dev/null
rm -f "$(pid_file "$name")"
log_info "已停止: ${name}"
}
# 重启隧道
tunnel_restart() {
tunnel_stop "$1"
# tunnel_stop 已确保进程退出;这里短暂等待端口释放
sleep 0.3
tunnel_start "$1"
}
# 返回隧道状态:running / stopped / error
tunnel_status() {
local name="$1"
if pid_alive "$name"; then
# 进一步检查本地转发端口是否监听(remote 类型无法本地校验,只看进程)
tunnel_load "$name" _ss
if [ "${_ss_type}" = "local" ] && [ -n "${_ss_bind_port}" ]; then
if port_listening 127.0.0.1 "${_ss_bind_port}"; then echo "running"
else echo "error"
fi
else
echo "running"
fi
else
echo "stopped"
fi
}
# 输出某隧道的完整 ssh 隧道命令(含 -L/-R 转发参数,可直接复制执行)
# 逻辑:user 填了就 user@host,否则直接 host(交给 ~/.ssh/config 处理)
# port 填了且非 22 才加 -p
show_ssh_cmd() {
local name="$1"
tunnel_exists "$name" || { log_err "隧道不存在: $name"; return 1; }
tunnel_load "$name" _cmd
# 构造 ssh 目标:有用户则 user@host,否则仅 host
local target
if [ -n "${_cmd_ssh_user}" ]; then
target="${_cmd_ssh_user}@${_cmd_ssh_host}"
else
target="${_cmd_ssh_host}"
fi
local port_opt=""
[ -n "${_cmd_ssh_port}" ] && [ "${_cmd_ssh_port}" != "22" ] && port_opt="-p ${_cmd_ssh_port}"
# 转发参数:local 用 -L,remote 用 -R
local fwd_opt
fwd_opt=$(build_forward_opt "$name") || { log_err "无法构造转发参数: $name"; return 1; }
# 拼接:ssh [-p port] -N -L/-R ... target
# -N 不执行远程命令,纯转发
echo "ssh ${port_opt}${port_opt:+ }-N ${fwd_opt} ${target}"
}
# 批量:启动全部
tunnel_start_all() {
local name
for name in $(tunnels_list); do
tunnel_start "$name"
done
}
# 批量:停止全部
tunnel_stop_all() {
local name
for name in $(tunnels_list); do
tunnel_stop "$name"
done
}
# ============== 交互式表单 ==============
# 读取一行输入,支持默认值(bash 3.2 兼容:用 -e -i)
prompt() {
local prompt_text="$1" default="${2:-}" var="$3"
local input
# 兼容 macOS bash 3.2(不支持 read -i)
# 默认值显示在提示后括号里;用户回车则采用默认值
if [ -n "$default" ]; then
prompt_text="${prompt_text} [${default}]: "
fi
read -e -p "$prompt_text" input || return $?
# 回车保留默认值
[ -z "$input" ] && input="$default"
# 通过 nameref 风格返回:调用方传入变量名
# bash 3.2 不支持 local -n,这里用 eval
eval "$var=\"\$input\""
return 0
}
# 新增或编辑隧道(编辑时传入旧名称)
tunnel_form() {
local old_name="${1:-}" mode
[ -n "$old_name" ] && mode="编辑" || mode="新增"
echo ""
echo "=== ${mode}隧道 ==="
echo "1) local (本地转发 -L:本地端口转发到服务器上的服务)"
echo "2) remote (远程转发 -R:把本地服务暴露到服务器端口)"
# 先声明所有本地变量,避免 set -u 下重复声明把已赋值变量置空
local type_choice name type ssh_host ssh_port ssh_user
local bind_host bind_port target_host target_port auto_start auto_reconnect
type_choice=""
prompt "选择类型 [1/2]: " "" type_choice || return 1
case "$type_choice" in
2) type="remote" ;;
*) type="local" ;;
esac
# 编辑模式下预填旧值
if [ "$mode" = "编辑" ]; then
name="$old_name"
tunnel_load "$old_name" _ef
type="${_ef_type}"
ssh_host="${_ef_ssh_host}"
ssh_port="${_ef_ssh_port}"
ssh_user="${_ef_ssh_user}"
bind_host="${_ef_bind_host}"
bind_port="${_ef_bind_port}"
target_host="${_ef_target_host}"
target_port="${_ef_target_port}"
auto_start="${_ef_auto_start}"
auto_reconnect="${_ef_auto_reconnect}"
else
ssh_host=""; ssh_port="22"; ssh_user=""
if [ "$type" = "local" ]; then
bind_host="127.0.0.1"
else
bind_host="0.0.0.0"
fi
bind_port=""; target_host="127.0.0.1"; target_port=""
auto_start="0"; auto_reconnect="1"
fi
# 名称
while true; do
prompt "名称 (字母数字_-): " "$name" name || return 1
valid_name "$name" || { log_warn "名称非法"; continue; }
[ "$mode" = "新增" ] && tunnel_exists "$name" && { log_warn "名称已存在"; continue; }
break
done
prompt "SSH 主机 (IP 或 ~/.ssh/config 别名): " "$ssh_host" ssh_host || return 1
[ -z "$ssh_host" ] && { log_warn "SSH 主机必填,已取消"; return 1; }
prompt "SSH 端口 (默认 22): " "$ssh_port" ssh_port || return 1
[ -z "$ssh_port" ] && ssh_port="22"
valid_port "$ssh_port" || { log_warn "端口非法"; return 1; }
prompt "SSH 用户 (留空用默认): " "$ssh_user" ssh_user || return 1
if [ "$type" = "local" ]; then
prompt "本地监听地址 (默认 127.0.0.1): " "$bind_host" bind_host || return 1
prompt "本地监听端口: " "$bind_port" bind_port || return 1
prompt "服务器目标地址 (默认 127.0.0.1): " "$target_host" target_host || return 1
prompt "服务器目标端口: " "$target_port" target_port || return 1
else
prompt "服务器监听地址 (默认 0.0.0.0,需服务器 GatewayPorts yes): " "$bind_host" bind_host || return 1
prompt "服务器监听端口: " "$bind_port" bind_port || return 1
prompt "本地目标地址 (默认 127.0.0.1): " "$target_host" target_host || return 1
prompt "本地目标端口: " "$target_port" target_port || return 1
fi
valid_port "$bind_port" || { log_warn "监听端口非法"; return 1; }
valid_port "$target_port" || { log_warn "目标端口非法"; return 1; }
prompt "自动启动 (0/1,默认 0): " "$auto_start" auto_start || return 1
[[ "$auto_start" =~ ^[01]$ ]] || auto_start="0"
prompt "自动重连 (0/1,默认 1): " "$auto_reconnect" auto_reconnect || return 1
[[ "$auto_reconnect" =~ ^[01]$ ]] || auto_reconnect="1"
# 编辑模式下若改名,先删旧记录
if [ "$mode" = "编辑" ] && [ "$name" != "$old_name" ]; then
tunnel_delete "$old_name"
fi
tunnel_save "$name" "$type" "$ssh_host" "$ssh_port" "$ssh_user" \
"$bind_host" "$bind_port" "$target_host" "$target_port" \
"$auto_start" "$auto_reconnect"
log_info "已保存: $name"
return 0
}
# 选择一条隧道(返回名称到指定变量)
pick_tunnel() {
local var="$1"
local list
list=$(tunnels_list)
[ -z "$list" ] && { log_warn "尚无隧道配置"; return 1; }
echo ""
echo "选择隧道:"
local i=1 name
local names=()
for name in $list; do
printf '%d) %s\n' "$i" "$name"
names[i]=$name
i=$((i+1))
done
local choice
prompt "序号: " "" choice
[[ "$choice" =~ ^[0-9]+$ ]] || { log_warn "非法输入"; return 1; }
[ "$choice" -ge 1 ] && [ "$choice" -le ${#names[@]} ] || { log_warn "超出范围"; return 1; }
eval "$var=\"\${names[$choice]}\""
}
# 删除隧道(交互确认)
tunnel_delete_interactive() {
local name
pick_tunnel name || return
local ans=""
read -e -p "确认删除 [$name]?(y/N): " ans || ans=""
[[ "$ans" =~ ^[Yy]$ ]] || { log_info "已取消"; return; }
tunnel_stop "$name" >/dev/null
tunnel_delete "$name"
rm -f "$(pid_file "$name")" "$(log_file "$name")"
log_info "已删除: $name"
}
# ============== 显示 ==============
# 列表展示所有隧道及状态
show_list() {
echo ""
echo "=== 隧道列表 ==="
local names
names=$(tunnels_list)
[ -z "$names" ] && { echo "(空)"; return; }
printf '%-16s %-8s %-42s %-10s\n' "名称" "类型" "转发" "状态"
local name st
for name in $names; do
# 一次性读取所有字段
tunnel_load "$name" _sl
st=$(tunnel_status "$name")
# local 类型:本地 -> ssh_host -> 目标;remote 类型:服务器 <- 本地
if [ "${_sl_type}" = "local" ]; then
printf '%-16s %-8s %s:%s -> [%s] -> %s:%s %-10s\n' \
"$name" "${_sl_type}" "${_sl_bind_host}" "${_sl_bind_port}" \
"${_sl_ssh_host}" "${_sl_target_host}" "${_sl_target_port}" "$st"
else
printf '%-16s %-8s [%s]:%s <- %s:%s %-10s\n' \
"$name" "${_sl_type}" "${_sl_ssh_host}" "${_sl_bind_port}" \
"${_sl_target_host}" "${_sl_target_port}" "$st"
fi
done
}
# 查看某条隧道日志
show_log() {
local name
pick_tunnel name || return
local f
f=$(log_file "$name")
[ -f "$f" ] || { log_warn "无日志: $f"; return; }
echo "=== 日志: $name ($f) ==="
tail -n 50 "$f"
}
# ============== 自动启动 ==============
# 启动时拉起 auto_start=1 的隧道
auto_start_tunnels() {
local name
for name in $(tunnels_list); do
tunnel_load "$name" _as
[ "${_as_auto_start}" = "1" ] && tunnel_start "$name"
done
}
# ============== 主菜单 ==============
main_menu() {
while true; do
echo ""
echo "======== SSH 隧道管理 ========"
echo "1) 查看隧道列表"
echo "2) 启动隧道"
echo "3) 停止隧道"
echo "4) 重启隧道"
echo "5) 一键全部启动"
echo "6) 一键全部停止"
echo "7) 新增隧道"
echo "8) 编辑隧道"
echo "9) 删除隧道"
echo "l) 查看日志"
echo "0) 退出"
local choice
# 管道/EOF 场景下 read 会失败,此时退出菜单避免无限循环
prompt "选择: " "" choice || break
case "$choice" in
1) show_list ;;
2) local n; pick_tunnel n && tunnel_start "$n" ;;
3) local n; pick_tunnel n && tunnel_stop "$n" ;;
4) local n; pick_tunnel n && tunnel_restart "$n" ;;
5) tunnel_start_all ;;
6) tunnel_stop_all ;;
7) tunnel_form ;;
8) local n; pick_tunnel n && tunnel_form "$n" ;;
9) tunnel_delete_interactive ;;
l|L) show_log ;;
0) confirm_exit && break ;;
*) log_warn "无效选项" ;;
esac
done
}
# 退出处理:询问是否保留运行中的隧道
confirm_exit() {
local running=0 name
for name in $(tunnels_list); do
pid_alive "$name" && running=$((running+1))
done
if [ "$running" -gt 0 ]; then
echo "当前有 $running 条隧道运行中。"
local ans=""
read -e -p "退出时 [k]保留运行 / [s]全部停止 / [c]取消: " ans || ans=""
case "$ans" in
[Kk]) log_info "保留 ${running} 条隧道运行(PID 见 ${PID_DIR})" ;;
[Ss]) tunnel_stop_all ;;
*) return 1 ;;
esac
fi
return 0
}
# 信号处理:Ctrl-C 时也走退出确认
trap 'echo; confirm_exit && exit 0' INT
# ============== 入口 ==============
# 打印帮助信息
show_help() {
cat <<EOF
SSH 隧道管理工具
用法:
./ssh-tunnel.sh [命令] [参数]
命令:
(无) 进入交互菜单
menu 进入交互菜单
list 查看所有隧道列表及状态
start <name> 启动指定隧道
stop <name> 停止指定隧道
restart <name> 重启指定隧道
status <name> 查看指定隧道状态
cmd <name> 输出该隧道的完整 ssh 命令(含 -L/-R 转发参数,可直接执行)
start-all 启动所有隧道
stop-all 停止所有隧道
-h, --help 显示此帮助信息
配置与数据文件(位于脚本所在目录):
config 隧道配置文件(菜单自动维护,无需手编)
pids/ 运行中隧道的 PID 文件
logs/ 每条隧道的 ssh 日志
示例:
./ssh-tunnel.sh # 进入菜单,交互式增删改隧道
./ssh-tunnel.sh start web-dev # 启动 web-dev 隧道
./ssh-tunnel.sh cmd web-dev # 输出: ssh myserver
./ssh-tunnel.sh list # 查看列表
认证说明:
SSH 认证完全交给系统 ssh(密钥 / ~/.ssh/config / ssh-agent)
建议在 ~/.ssh/config 中配置 Host 别名,工具里 ssh_host 填别名即可
自动重连:
若安装了 autossh(brew install autossh),断线自动重连
EOF
}
main() {
init_dirs
# 支持命令行直接操作:ssh-tunnel.sh start <name> / stop <name> / list / start-all
if [ $# -ge 1 ]; then
case "$1" in
-h|--help) show_help; exit 0 ;;
list) show_list; exit 0 ;;
start-all) tunnel_start_all; exit 0 ;;
stop-all) tunnel_stop_all; exit 0 ;;
start) [ -n "${2:-}" ] && tunnel_start "$2"; exit 0 ;;
stop) [ -n "${2:-}" ] && tunnel_stop "$2"; exit 0 ;;
restart) [ -n "${2:-}" ] && tunnel_restart "$2"; exit 0 ;;
status) [ -n "${2:-}" ] && tunnel_status "$2"; exit 0 ;;
cmd) [ -n "${2:-}" ] && show_ssh_cmd "$2"; exit 0 ;;
menu|"") : ;; # 进入交互菜单
*) echo "用法: $0 [list|start <name>|stop <name>|restart <name>|status <name>|cmd <name>|start-all|stop-all|menu]"; exit 1 ;;
esac
fi
echo "SSH 隧道管理工具(配置: ${CONFIG_FILE})"
has_cmd autossh && echo "检测到 autossh,断线自动重连已启用" \
|| echo "提示: 未安装 autossh,断线不会自动重连(brew install autossh 可启用)"
auto_start_tunnels
main_menu
}
main "$@"
更多推荐

所有评论(0)