SSH 隧道管理与安全加固

 技术  frp  ssh  ssh隧道  端口转发 󰈭 2745字

本文详细描述了基于 SSH 隧道的端口转发解决方案, 替代了原有的 frp 方案. 通过 systemd 模板单元和辅助脚本, 实现了动态 IP 与静态 IP 并存的管理方式, 并将隧道配置与具体的执行过程解耦. 文章提供了 SSH 反向隧道的配置示例, 并探讨了安全加固措施, 包括 iptables 限流和 sshd 配置调整, 以应对公网上的暴力破解尝试.

前言

之前一直使用 frp 作为内网穿透的工具, 内网机器除了作为 client 去连接到具有公网 IP 的服务器以外, 还会通过内网连接到我自己在宿舍的一台电脑上, 这台电脑的 IP 是由校园网动态分配的, 所以该机器会定期上报自己的 IP 供 client 去动态获取并连接, 一些更多的细节在: Mac(x86派)的废物再利用 - frp转发.

其实相对来说 frp 作为一款成熟的穿透软件还是比较好用的, 但是在我的集成&使用过程中还是会有一些问题:

  • 最严重的是一个使用上的问题: frpc 程序如果启动时发现 frps 无法连接, 就会直接退出程序; 但如果最开始可以连接, 中途发现无法连接了, 其还是会继续运行, 并不断尝试连接该 frps. 这样本身的逻辑是还算可以接受的, 但是在我的内网动态 IP 连接的场景下会存在问题:

    • 我使用 systemd 去管理 frpc 的运行, 真正 EXEC 之前会前拉取我宿舍机器上报的 IP, 然后判断是否 ping 通看电脑在不在线, 在线则开始执行 systemd 的 EXEC 任务.. 但悲摧的是最近发现校园网 IP 经常变了, 以前不变的时候可以与 frpc 默认的逻辑工作的很好, 但现在经常变动就意味着我需要 frpc 在连接过程中一旦失败就停止, 这样我才能将其整合进我的整个 systemd 流程中.. 现在 frpc 的策略会使其不断在一个旧的 IP 上尝试, 同时不会 systemd 任务失败导致我的工作流重启获取到最新的 IP..

    • 当然我们总是可以通过一些脚本去解决这个问题, 或者另外起一个进程去管控这个 systemd 任务, 当目标不通时自动去kill掉这个进程让它重启 systemd 流程.. 但我想这样实在是过于丑陋了.. 而且过于依赖 frp 本身的逻辑, 感觉不好

  • 另一个恼人的主要是配置上的问题: 我目前尝试将所有的自定义脚本与配置全部整合进入我的 stow 中, 而 frpc 默认是需要使用 yaml/toml/ini 等实体文件来做服务的配置的, 虽然说也还行.. 大概.. 但相比于纯粹的原生命令行工具, 能够使用参数进行配置是更加简单且幸福的事情..

  • frp 还有的问题主要是 token 可能遭受的攻击. 公网上的服务器就像时刻遭到太阳风暴的星球一样, 无时不刻不在遭受端口爆破与扫描.. 这也是前几天本地磁盘坏了看系统日志才发现的, 暴露出去的 ssh 被持续地爆破尝试, 可想而知 frp token 也正在遭遇持续不断的攻击.

  • 以及, frp 并不是一个很原生的工具, 尽管一些发行版(如archlinux)会在安装包中内置一个 systemd 的 service 供用户使用, 但像在 debian 等发行版上, 通常我是去 github release 单独下载的, 还需要解决一下如何常驻 frps 的问题.. 会有一些麻烦;

    • 而对于 ssh 转发来说, 只需要在服务器上将 GatewayPorts 参数设置为 yes 即可

基于这样的理由, 加上 ssh-tunnel 看上去更加的原生且足够简单并能够满足我的需求, 因此最终搬迁我的端口转发功能到 ssh 隧道上.

SSH 隧道管理

本文主要使用反向 SSH 隧道去做端口的暴露, 其基本的指令为:

Text
1$ ssh -R <remote_port>:localhost:<local_port> <user>@<remote_host>

即可建立一条 localhost:<local_port> 到 <remote_host>:<remote_port> 的隧道.

为了持久化 SSH 隧道, 通常还加上额外的参数:

  • -N: 不执行远程命令, 只转发端口

  • ServerAliveInterval=60: 每 60 秒发送一个心跳包, 保持连接

  • ExitOnForwardFailure=yes: 如果转发失败, 则退出

完整的命令大概长这样:

bash
1exec ssh -R "${REMOTE_PORT}:${LOCAL_HOST}:${LOCAL_PORT}" \
2    -N \
3    -o "ServerAliveInterval=60" \
4    -o "ExitOnForwardFailure=yes" \
5    "${REMOTE_USER}@${SERVER_IP}"

这样其他人就可以通过 ssh -p <remote_port> <user>@<remote_host> 来连接到暴露出去的端口了.

足够简单.

为了解耦配置与执行, 使用 systemd 模板单元 去创建多个服务实例, 用户只需要添加额外的 conf 文件即可拓展端口转发能力. 其结构图如下:

ssh 隧道管理目录结构

在实现中, 由于我希望实现静态 IP 与动态 IP 并存, 我将具体的转发逻辑拆分到了一个单独的辅助脚本 ssh-tunnel-connect.sh 中, 其主要根据变量 IP_SERVERREMOTE_SERVER 来决定具体的 IP 获取策略. 当前置的变量获取任务全部完成后, 随后即可使用 exec 将程序替换成一个通常的 ssh 反向隧道了.

bash
 1#!/bin/bash
 2
 3# 检查必要的环境变量
 4if [ -z "$REMOTE_PORT" ] || [ -z "$LOCAL_HOST" ] || [ -z "$LOCAL_PORT" ] || [ -z "$REMOTE_USER" ]; then
 5    echo "错误: 必要的环境变量未设置"
 6    exit 1
 7fi
 8
 9# 确定要连接的服务器IP
10if [ -n "$IP_SERVER" ]; then
11    echo "使用动态IP服务: $IP_SERVER"
12    SERVER_IP=$(curl -s "$IP_SERVER")
13    echo "获取到IP: $SERVER_IP"
14else
15    SERVER_IP="$REMOTE_SERVER"
16    echo "使用配置的服务器: $SERVER_IP"
17fi
18
19# 检查服务器是否可连接
20echo "检查服务器连接性..."
21nc -z -w 5 "$SERVER_IP" 22 >/dev/null 2>&1
22if [ $? -ne 0 ]; then
23    echo "错误: 无法连接到服务器 $SERVER_IP 的SSH端口 (22)"
24    exit 1
25fi
26
27# 建立SSH隧道
28echo "建立隧道: ${REMOTE_USER}@${SERVER_IP}:${REMOTE_PORT} -> ${LOCAL_HOST}:${LOCAL_PORT}"
29exec ssh -R "${REMOTE_PORT}:${LOCAL_HOST}:${LOCAL_PORT}" \
30    -N \
31    -o "ServerAliveInterval=60" \
32    -o "ExitOnForwardFailure=yes" \
33    "${REMOTE_USER}@${SERVER_IP}"

由于将具体的执行单元进一步做了拆分, systemd 的程序就可以变得很简单, 只需要基本指定一下模板所使用的配置信息即可.

Text
 1[Unit]
 2Description=SSH Tunnel for %i
 3After=network-online.target
 4Wants=network-online.target
 5
 6[Service]
 7# 使用环境文件加载特定隧道的配置
 8EnvironmentFile=%h/.config/ssh-tunnel/%i.conf
 9# 使用专用脚本处理隧道连接
10ExecStart=%h/.config/ssh-tunnel/ssh-tunnel-connect.sh
11
12Restart=always
13RestartSec=30
14
15[Install]
16WantedBy=default.target

这样, 用户只需要在 ~/.config/ssh-tunnel/ 下创建一个 *.conf 的配置文件即可, 例如:

bash
 1# SSH隧道示例配置文件
 2# 复制此文件并重命名为你的隧道名称.conf (如 web.conf, ssh.conf 等)
 3
 4# 远程服务器信息
 5REMOTE_SERVER=your-server.example.com
 6# 动态IP解析服务器(可选, 留空则使用REMOTE\_SERVER)
 7# 通过 curl -s ${IP_SERVER} 获取动态IP
 8IP_SERVER=
 9
10REMOTE_USER=your_username
11REMOTE_PORT=12345
12
13# 本地服务信息
14LOCAL_HOST=localhost
15LOCAL_PORT=22

随后通过 systemctl --user start ssh-tunnel@<name> 即可启动该隧道.

这样实现可优美的多了!

最后, 为了安全上传到 github 等公开的仓库( 代码示例)中, ignore 一下即可:

Text
1# 忽略所有实际SSH隧道配置文件,但保留示例文件
2**/ssh-tunnel/*.conf
3!**/ssh-tunnel/example.conf

SSH 访问加固

由于我本身的 SSH 访问已经是只允许公私钥登录, 禁止密码登录了, 但奈何还是看不得 sshd 那一堆乱飞的登录请求像苍蝇一样干扰我的日志..

但由于我是基于隧道做转发的, fail2ban 这种基于 IP 的工具好像就不太合适了, 因为在我这里所有的 IP 都是 localhost 来的.. 而我本身也只是希望处理一下苍蝇, 并不担心真正的安全性, 所以最终在公网服务器上用 iptables 做一下限流即可.

Text
1# 限制每IP每分钟最多3次新SSH连接, 实际使用需将 22 替换为实际的端口
2iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
3iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP

查了一圈, 为了持久化 iptables 的配置还蛮麻烦的, 如果要原生, 就利用 systemd 会自动 iptables-restore 之前导出的规则.. 如果不担心环境脏掉, debian 系就直接安装 iptables-persistent, 一键保存:

bash
1$ apt install iptables-persistent
2$ netfilter-persistent save

SSH 配置加固

之前为了加固 sshd 去做了一些配置上的调整.. 把 MaxAuthTries 变成 1 了, 但这其实会阻碍正常的 ssh 连接.. 因为一次连接中会进行多次尝试, 看本地的哪个密钥是正确的.. 而通常来说一个人有个 rsa 密钥, 有个 ed25519 密钥, 再有点别的什么都是正常的.. 除非用户手动通过 -i xxx 去指定自己的 ssh 使用的是哪个密钥文件.

因此最后我将 MaxAuthTries 设置为 3, 这样自己使用的时候可以无感正常连接, 而暴力破解可能会受到一些的阻碍(虽然我看到现在好像都是基于密码的登录请求, 毕竟密钥是不太可能暴力的.. 可能这个加固方式也用处不大)

抗攻击的用户名(bushi

XD 另一个好笑的事情是, 我的用户名并不是一个常见的用户名.. 甚至可以说是全互联网独一无二的用户名, 感觉上是没有被收录到字典里的, 所以看到他的一堆 Alice 和 Bob 这种类型的用户名的的登录请求还是感觉到很好笑 🤣

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱; 加密博客访问请求等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改日志
  • 2025-04-10 21:54:59 SSH 隧道管理与安全加固