在很多场景下,我们需要从外网访问内网服务器:

  • 家庭/公司内网的开发环境
  • 位于 NAT/防火墙后的服务器
  • 临时需要远程协助的内网机器

传统的端口映射需要路由器配置或公网IP,而反向SSH隧道提供了一种更灵活的解决方案。

本文将从零开始,讲解反向SSH隧道的配置原理,以及如何通过 autossh + systemd + 健康检查 构建一个高可用的内网穿透方案。


一、反向SSH隧道原理

1.1 正向 vs 反向隧道

正向隧道(Local Port Forwarding)

1
本地机器 → SSH服务器 → 目标服务

应用场景:访问远程服务器上的内网服务

反向隧道(Remote Port Forwarding)

1
2
公网服务器 ← SSH连接 ← 内网机器
公网服务器端口 → 隧道 → 内网机器SSH端口

应用场景:从公网访问内网机器

1.2 反向隧道工作流程

  1. 内网机器主动建立SSH连接到公网服务器
  2. 公网服务器开放一个端口(如 19922)
  3. 访问公网服务器的 19922 端口,流量会被转发到内网机器的 22 端口
  4. 外网用户通过 ssh -p 19922 user@public-server 即可登录内网机器

二、基础配置

2.1 环境要求

公网服务器

  • 有公网IP
  • 运行SSH服务
  • 允许端口转发

内网机器

  • 能访问公网服务器(出站SSH)
  • 运行SSH服务

2.2 手动建立反向隧道

1
2
3
4
5
6
7
8
9
# 在内网机器上执行
ssh -N -R 19922:localhost:22 user@public-server

# 参数说明:
# -N: 不执行远程命令,只做端口转发
# -R: 反向隧道
# 19922: 公网服务器上开放的端口
# localhost:22: 内网机器的SSH端口
# user@public-server: 公网服务器

2.3 测试连接

1
2
3
4
5
# 从外网机器连接内网
ssh -p 19922 internal-user@public-server

# 或通过跳板方式(如果公网服务器限制 GatewayPorts)
ssh -J user@public-server -p 19922 localhost

2.4 GatewayPorts 配置

问题:默认情况下,反向隧道只绑定到 localhost,外部无法访问。

解决:修改公网服务器的 /etc/ssh/sshd_config

1
2
3
4
5
6
7
8
# 启用 GatewayPorts
GatewayPorts yes

# 或使用 clientspecified(推荐,更安全)
# GatewayPorts clientspecified

# 重启SSH服务
sudo systemctl restart sshd

然后在建立隧道时指定绑定地址:

1
2
3
4
5
# 绑定到所有接口(0.0.0.0)
ssh -N -R 0.0.0.0:19922:localhost:22 user@public-server

# 或只绑定到特定IP
ssh -N -R 1.2.3.4:19922:localhost:22 user@public-server

三、使用 autossh 实现自动重连

3.1 为什么需要 autossh?

手动建立的SSH隧道有以下问题:

  • 网络抖动导致连接断开
  • 服务器重启后隧道丢失
  • 无法自动恢复

autossh 是一个自动重启SSH会话的工具,能检测连接状态并自动重连。

3.2 安装 autossh

1
2
3
4
5
6
7
8
9
# Ubuntu/Debian
sudo apt update
sudo apt install autossh

# CentOS/RHEL
sudo yum install autossh

# macOS
brew install autossh

3.3 autossh 基础用法

1
2
3
4
5
6
7
8
# 基础命令
autossh -M 0 -N -o "ServerAliveInterval 30" -o "ServerAliveCountMax 3" \
  -R 19922:localhost:22 user@public-server

# 参数说明:
# -M 0: 禁用监控端口(依赖SSH的心跳机制)
# -o ServerAliveInterval 30: 每30秒发送心跳
# -o ServerAliveCountMax 3: 3次心跳失败后断开

推荐做法:启用监控端口

1
2
3
4
5
6
7
8
9
# 启用监控端口(推荐)
autossh -M 19923 -N \
  -o "ServerAliveInterval 30" \
  -o "ServerAliveCountMax 3" \
  -R 19922:localhost:22 user@public-server

# 参数说明:
# -M 19923: 使用19923端口监控SSH连接状态
# autossh会通过该端口检测连接是否存活

3.4 autossh 环境变量

1
2
3
4
# 设置环境变量优化autossh行为
export AUTOSSH_GATETIME=30     # 启动后等待30秒确认稳定
export AUTOSSH_POLL=60         # 每60秒检查一次
export AUTOSSH_FIRST_POLL=30   # 首次检查在30秒后

四、systemd 服务化管理

4.1 创建 systemd 服务

创建文件 /etc/systemd/system/reverse-tunnel.service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[Unit]
Description=AutoSSH Reverse Tunnel
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=your-username
Environment="AUTOSSH_GATETIME=30"
Environment="AUTOSSH_POLL=60"
Environment="AUTOSSH_FIRST_POLL=30"

ExecStart=/usr/bin/autossh \
    -M 19923 \
    -N \
    -o "ServerAliveInterval=30" \
    -o "ServerAliveCountMax=3" \
    -o "ExitOnForwardFailure=yes" \
    -o "StrictHostKeyChecking=accept-new" \
    -o "BatchMode=yes" \
    -R 0.0.0.0:19922:localhost:22 \
    user@public-server

# 重启策略
Restart=on-failure
RestartSec=10s
StartLimitInterval=300s
StartLimitBurst=5

# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=reverse-tunnel

[Install]
WantedBy=multi-user.target

4.2 关键配置说明

SSH 参数优化

1
2
3
-o "ExitOnForwardFailure=yes"      # 端口转发失败立即退出
-o "StrictHostKeyChecking=accept-new"  # 自动接受新主机密钥
-o "BatchMode=yes"                  # 非交互模式(适合自动化)

重启策略

1
2
3
4
Restart=on-failure          # 失败时重启
RestartSec=10s             # 重启间隔10秒
StartLimitInterval=300s    # 5分钟内
StartLimitBurst=5          # 最多重试5次

防止无限重启导致系统资源耗尽。

网络依赖

1
2
After=network-online.target
Wants=network-online.target

确保网络就绪后再启动服务。

4.3 管理服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 启动服务
sudo systemctl start reverse-tunnel

# 开机自启
sudo systemctl enable reverse-tunnel

# 查看状态
sudo systemctl status reverse-tunnel

# 查看日志
sudo journalctl -u reverse-tunnel -f

# 重启服务
sudo systemctl restart reverse-tunnel

# 停止服务
sudo systemctl stop reverse-tunnel

五、健康检查与主动维持

5.1 问题:被动重连不够

即使使用了 autossh + systemd,仍可能遇到:

  • 隧道进程存在,但实际不可用
  • 网络长时间中断后无法恢复
  • 端口被占用或转发失败

解决方案:主动健康检查

5.2 创建健康检查脚本

创建文件 /usr/local/bin/check-reverse-tunnel.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/bin/bash
# 反向隧道健康检查脚本

# ==================== 配置参数 ====================
REMOTE_HOST="your-public-server.com"  # 公网服务器地址
REMOTE_PORT="22"                       # 公网服务器SSH端口
REMOTE_USER="your-username"            # 公网服务器用户名
FORWARD_PORT="19922"                   # 反向隧道端口
SERVICE_NAME="reverse-tunnel"          # systemd服务名
LOG_FILE="/var/log/reverse-tunnel.log" # 日志文件
# =================================================

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

# 检查服务是否运行
if ! systemctl is-active --quiet $SERVICE_NAME; then
    log "服务未运行,尝试重启..."
    systemctl restart $SERVICE_NAME
    sleep 5
fi

# 测试隧道是否真的可用
# 方法:通过公网服务器检查端口是否监听
if ssh -p $REMOTE_PORT -o ConnectTimeout=5 -o BatchMode=yes $REMOTE_USER@$REMOTE_HOST \
   "nc -z localhost $FORWARD_PORT" 2>/dev/null; then
    log "健康检查通过:隧道正常"
    exit 0
else
    log "健康检查失败:隧道不可用,重启服务..."
    systemctl restart $SERVICE_NAME
    sleep 10
    
    # 再次检查
    if ssh -p $REMOTE_PORT -o ConnectTimeout=5 -o BatchMode=yes $REMOTE_USER@$REMOTE_HOST \
       "nc -z localhost $FORWARD_PORT" 2>/dev/null; then
        log "重启后隧道恢复正常"
        exit 0
    else
        log "重启后隧道仍然不可用,需要人工介入"
        # 这里可以添加告警逻辑(发送邮件、短信等)
        exit 1
    fi
fi
1
2
# 赋予执行权限
sudo chmod +x /usr/local/bin/check-reverse-tunnel.sh

5.3 添加定时任务

使用 crontab 定期执行健康检查:

1
2
3
4
5
# 编辑 crontab
sudo crontab -e

# 添加以下内容(每分钟检查一次)
*/1 * * * * /usr/local/bin/check-reverse-tunnel.sh

5.4 告警机制(可选)

在健康检查脚本中添加告警逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 发送邮件告警
send_alert() {
    local message=$1
    echo "$message" | mail -s "反向隧道告警" your-email@example.com
}

# 或调用Webhook
send_webhook() {
    local message=$1
    curl -X POST -H 'Content-Type: application/json' \
        -d "{\"text\":\"$message\"}" \
        https://your-webhook-url
}

六、完整配置脚本示例

以下是一个完整的配置脚本,整合了上述所有功能:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#!/bin/bash
set -e

# ==================== 配置参数 ====================
REMOTE_USER="your-username"
REMOTE_HOST="your-public-server.com"
REMOTE_PORT="22"
LOCAL_SSH_PORT="22"
REMOTE_FORWARD_PORT="19922"
MONITOR_PORT="19923"
SERVICE_NAME="reverse-tunnel"
LOG_FILE="/var/log/reverse-tunnel.log"
# =================================================

# 日志函数
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | sudo tee -a "$LOG_FILE"
}

# 检查依赖
check_dependencies() {
    log "检查依赖..."
    for cmd in ssh autossh curl nc; do
        if ! command -v $cmd &> /dev/null; then
            log "安装 $cmd..."
            sudo apt update && sudo apt install -y $cmd
        fi
    done
}

# 测试SSH连接
test_connection() {
    log "测试SSH连接..."
    if ! ssh -p $REMOTE_PORT -o ConnectTimeout=5 -o BatchMode=yes \
         $REMOTE_USER@$REMOTE_HOST "echo 连接成功" &>/dev/null; then
        log "错误:无法连接到公网服务器"
        return 1
    fi
    log "SSH连接测试成功"
}

# 检查GatewayPorts
check_gateway_ports() {
    log "检查GatewayPorts设置..."
    if timeout 5 ssh -p $REMOTE_PORT \
         -R 0.0.0.0:$REMOTE_FORWARD_PORT:localhost:$LOCAL_SSH_PORT \
         $REMOTE_USER@$REMOTE_HOST "exit" 2>/dev/null; then
        log "GatewayPorts已启用"
        echo "0.0.0.0"
    else
        log "GatewayPorts未启用,仅绑定到localhost"
        echo "localhost"
    fi
}

# 创建systemd服务
create_service() {
    local bind_addr=$1
    
    log "创建systemd服务..."
    sudo bash -c "cat > /etc/systemd/system/$SERVICE_NAME.service" <<EOF
[Unit]
Description=AutoSSH Reverse Tunnel
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=$USER
Environment="AUTOSSH_GATETIME=30"
Environment="AUTOSSH_POLL=60"
Environment="AUTOSSH_FIRST_POLL=30"

ExecStart=/usr/bin/autossh \\
    -M $MONITOR_PORT \\
    -N \\
    -o "ServerAliveInterval=30" \\
    -o "ServerAliveCountMax=3" \\
    -o "ExitOnForwardFailure=yes" \\
    -o "StrictHostKeyChecking=accept-new" \\
    -o "BatchMode=yes" \\
    -p $REMOTE_PORT \\
    -R ${bind_addr}:$REMOTE_FORWARD_PORT:localhost:$LOCAL_SSH_PORT \\
    $REMOTE_USER@$REMOTE_HOST

Restart=on-failure
RestartSec=10s
StartLimitInterval=300s
StartLimitBurst=5

StandardOutput=journal
StandardError=journal
SyslogIdentifier=$SERVICE_NAME

[Install]
WantedBy=multi-user.target
EOF

    sudo systemctl daemon-reload
    sudo systemctl enable $SERVICE_NAME
    sudo systemctl restart $SERVICE_NAME
    
    log "服务已启动: $SERVICE_NAME"
}

# 创建健康检查脚本
create_health_check() {
    log "创建健康检查脚本..."
    
    sudo bash -c "cat > /usr/local/bin/check-$SERVICE_NAME.sh" <<EOF
#!/bin/bash
REMOTE_HOST="$REMOTE_HOST"
REMOTE_PORT="$REMOTE_PORT"
REMOTE_USER="$REMOTE_USER"
FORWARD_PORT="$REMOTE_FORWARD_PORT"
SERVICE_NAME="$SERVICE_NAME"
LOG_FILE="$LOG_FILE"

log() {
    echo "[\$(date '+%Y-%m-%d %H:%M:%S')] \$1" >> "\$LOG_FILE"
}

if ! systemctl is-active --quiet \$SERVICE_NAME; then
    log "服务未运行,重启..."
    systemctl restart \$SERVICE_NAME
    sleep 5
fi

if ssh -p \$REMOTE_PORT -o ConnectTimeout=5 -o BatchMode=yes \$REMOTE_USER@\$REMOTE_HOST \\
   "nc -z localhost \$FORWARD_PORT" 2>/dev/null; then
    log "健康检查通过"
    exit 0
else
    log "健康检查失败,重启..."
    systemctl restart \$SERVICE_NAME
    sleep 10
    
    if ssh -p \$REMOTE_PORT -o ConnectTimeout=5 -o BatchMode=yes \$REMOTE_USER@\$REMOTE_HOST \\
       "nc -z localhost \$FORWARD_PORT" 2>/dev/null; then
        log "重启后正常"
        exit 0
    else
        log "重启后仍失败"
        exit 1
    fi
fi
EOF

    sudo chmod +x /usr/local/bin/check-$SERVICE_NAME.sh
    
    # 添加到crontab
    (sudo crontab -l 2>/dev/null | grep -v "check-$SERVICE_NAME"; \
     echo "*/1 * * * * /usr/local/bin/check-$SERVICE_NAME.sh") | sudo crontab -
    
    log "健康检查已配置"
}

# 显示连接信息
show_info() {
    local bind_addr=$1
    
    echo ""
    echo "================= 配置完成 ================="
    echo "公网服务器: $REMOTE_HOST:$REMOTE_PORT"
    echo "本地端口: localhost:$LOCAL_SSH_PORT"
    echo "映射端口: ${bind_addr}:$REMOTE_FORWARD_PORT"
    echo ""
    echo "连接命令:"
    if [ "$bind_addr" = "0.0.0.0" ]; then
        echo "  ssh -p $REMOTE_FORWARD_PORT \$USER@$REMOTE_HOST"
    else
        echo "  ssh -J $REMOTE_USER@$REMOTE_HOST:$REMOTE_PORT -p $REMOTE_FORWARD_PORT localhost"
    fi
    echo ""
    echo "管理命令:"
    echo "  查看状态: sudo systemctl status $SERVICE_NAME"
    echo "  查看日志: sudo journalctl -u $SERVICE_NAME -f"
    echo "  重启服务: sudo systemctl restart $SERVICE_NAME"
    echo "============================================"
}

# 主函数
main() {
    sudo touch "$LOG_FILE"
    sudo chown $USER:$USER "$LOG_FILE"
    
    log "===== 开始配置反向隧道 ====="
    
    check_dependencies
    test_connection || exit 1
    
    BIND_ADDR=$(check_gateway_ports)
    create_service "$BIND_ADDR"
    create_health_check
    
    show_info "$BIND_ADDR"
    
    log "===== 配置完成 ====="
}

main "$@"

七、最佳实践与注意事项

7.1 安全建议

  1. 使用SSH密钥认证

    1
    2
    3
    4
    5
    
    # 生成密钥对(如果还没有)
    ssh-keygen -t ed25519 -C "reverse-tunnel"
    
    # 复制公钥到公网服务器
    ssh-copy-id -p 22 user@public-server
  2. 限制公网服务器上的端口访问

    1
    2
    3
    
    # 在公网服务器上配置防火墙
    # 只允许特定IP访问19922端口
    sudo ufw allow from 1.2.3.4 to any port 19922
  3. 使用非标准端口

    • 避免使用 22、2222 等常见端口
    • 使用 10000-65535 之间的高位端口
  4. 定期更换密钥

    1
    2
    
    # 每半年更换一次SSH密钥
    ssh-keygen -t ed25519 -C "reverse-tunnel-$(date +%Y%m)"

7.2 性能优化

  1. 启用SSH压缩

    1
    
    -o "Compression=yes"
  2. 调整心跳间隔

    1
    2
    3
    
    # 网络不稳定时,增加心跳频率
    -o "ServerAliveInterval=15"
    -o "ServerAliveCountMax=5"
  3. 使用ControlMaster(可选)

    1
    2
    3
    4
    
    # 复用SSH连接,减少连接开销
    -o "ControlMaster=auto"
    -o "ControlPath=/tmp/ssh-%r@%h:%p"
    -o "ControlPersist=600"

7.3 故障排查

问题1:隧道无法建立

1
2
3
4
5
6
7
8
# 检查公网服务器SSH配置
sudo grep GatewayPorts /etc/ssh/sshd_config

# 检查端口是否被占用
sudo netstat -tulpn | grep 19922

# 查看详细日志
sudo journalctl -u reverse-tunnel -n 100

问题2:隧道频繁断开

1
2
3
4
5
# 检查网络稳定性
ping public-server

# 调整心跳参数
# 增加 ServerAliveInterval 和 ServerAliveCountMax

问题3:连接成功但无法登录

1
2
3
4
5
6
7
8
# 检查内网机器的SSH服务
sudo systemctl status sshd

# 检查防火墙
sudo ufw status

# 检查SSH配置
sudo grep "PermitRootLogin\|PasswordAuthentication" /etc/ssh/sshd_config

八、替代方案对比

方案优点缺点适用场景
反向SSH隧道无需额外工具,安全性高需要公网服务器临时访问、技术团队
frp功能强大,支持多种协议需要独立部署长期稳定访问
ngrok无需公网服务器,即开即用免费版限制多临时测试、演示
Tailscale零配置,P2P直连依赖第三方服务个人使用、小团队
ZeroTier去中心化,支持自建配置复杂企业级组网

结语

反向SSH隧道是一种简单而强大的内网穿透方案,配合 autossh + systemd + 健康检查,可以实现接近99.9%的可用性。

核心要点回顾

  1. ✅ 启用 autossh 监控端口(不要用 -M 0
  2. ✅ 使用 systemd 管理服务,配置合理重启策略
  3. ✅ 添加定时健康检查,主动发现问题
  4. ✅ 完善日志记录,便于故障排查
  5. ✅ 注意安全配置,限制端口访问

希望这篇文章能帮助你构建稳定可靠的内网穿透方案!


参考资料


最后更新:2026-03-05