服务端有多个服务希望在本地访问,并且不想开放那么多端口,可以使用这个工具 。
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 "$@"

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐