家里的 Linux 主机可以运行 Cockpit、开发环境、监控面板和文件服务,但家庭宽带往往没有公网 IPv4。所谓”内网穿透”,本质上是为外部设备建立一条能够到达内网服务的路径。

这篇文章介绍四种常见方案:

  1. 直接使用运营商分配的公网 IPv6;
  2. 通过有公网 IP 的 VPS 建立 SSH 反向隧道;
  3. 使用 Cloudflare Tunnel 发布 Web 或 SSH 服务;
  4. 使用 Tailscale 组建只对授权设备开放的私有网络。

**安全原则:**远程访问不等于把服务暴露给整个互联网。SSH、Cockpit、数据库和管理面板应优先使用私有网络或身份认证;只有确实需要公开访问的 Web 服务,才应配置公开域名。

1. 先选择合适的方案

方案额外条件最适合是否公开服务端口主要限制
公网 IPv6运营商提供可入站的 IPv6自己管理的 SSH、HTTPS 服务地址可能变化,路由器和主机防火墙必须正确配置
SSH 反向隧道一台有公网 IP 的 VPS临时转发、完全自控的运维通道VPS 至少开放 SSH需要维护 VPS、密钥和长连接
Cloudflare TunnelCloudflare 账户;公开 hostname 还需要托管域名对外发布 Web 应用,或通过 Access 访问 SSHSSH 等非 HTTP 服务需要客户端 cloudflared 或 Cloudflare One Client
Tailscale访问端和目标端安装客户端个人设备、团队运维、整个家庭局域网访问设备需要加入同一 tailnet;依赖 Tailscale 控制平面
flowchart LR
  A[需要从外部访问内网] --> B{访问者是否都能安装客户端}
  B -->|可以| C[Tailscale]
  B -->|不可以| D{是否为公开 Web 应用}
  D -->|| E[Cloudflare Tunnel + Access/WAF]
  D -->|| F{有可入站的公网 IPv6}
  F -->|| G[IPv6 + 防火墙 + SSH/HTTPS]
  F -->|没有| H[SSH 反向隧道 + VPS]

对个人和家庭环境,我通常优先选择 Tailscale:不需要域名、VPS 或路由器端口映射。需要让普通访客访问 Web 应用时,再选择 Cloudflare Tunnel。公网 IPv6 和 SSH 反向隧道则适合希望自己控制网络路径的人。

2. 使用公网 IPv6 直接访问

公网 IPv4 稀缺并不代表家庭网络一定没有公网地址。很多运营商会下发全球单播 IPv6 地址;只要入站流量没有被运营商或路由器拦截,就可以直接访问家中的主机。

2.1 确认主机获得了全球 IPv6 地址

在内网主机上检查地址和默认路由:

ip -6 address show scope global
ip -6 route show default

fe80::/10 是链路本地地址,只能在同一二层网络使用;fc00::/7 属于唯一本地地址,也不能从公网直接访问。这里需要的是具有公网路由能力的全球单播地址。各类地址的定义可以查阅 RFC 4291

接着确认主机可以主动访问 IPv6 网络:

ping -6 -c 4 2606:4700:4700::1111

主动访问成功仍不代表公网可以入站。还需要检查光猫或路由器的 IPv6 防火墙。IPv6 通常不需要 IPv4 风格的 NAT 端口转发,但家用路由器常会默认拒绝所有入站连接。

2.2 配置 SSH 和防火墙

先确认 SSH 正在监听 IPv6:

sudo ss -lntp | grep ':22'

输出中出现 [::]:22,表示 sshd 正在 IPv6 地址上监听。使用 firewalld 的系统可以放行 SSH 服务:

sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-services

还要在路由器的 IPv6 入站规则中放行 TCP 22。能限制来源 IPv6 前缀时,应只允许自己的办公网络或移动设备地址,而不是向所有来源开放。

在关闭密码登录之前,先配置并验证公钥:

ssh-keygen -t ed25519 -a 100
ssh-copy-id [email protected]

确认新终端能够用密钥登录后,再创建 /etc/ssh/sshd_config.d/50-hardening.conf

PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no

检查配置后平滑重新加载 sshd,不要直接中断当前会话:

sudo sshd -t
sudo systemctl reload sshd

2.3 配置 AAAA 记录并测试

如果 IPv6 地址稳定,可以为域名添加一条 DNS AAAA 记录:

home.example.com.  AAAA  2001:db8:1234:5678::10

这里的 2001:db8::/32 是说明文档示例地址,必须替换为自己的真实地址。若用 Cloudflare 管理 DNS,直接连接 SSH 的 AAAA 记录应设为 DNS only;普通 Cloudflare 代理并不会把原始 SSH 连接转发到主机。

然后在一条真正的外部 IPv6 网络上测试,而不是在家中 Wi-Fi 内测试:

ssh -6 [email protected]

如果运营商会定期更换 IPv6 前缀,需要配置动态 DNS。地址或前缀频繁变化时,Tailscale 或 Cloudflare Tunnel 通常更省心。

3. 使用 SSH 反向隧道

SSH 远程端口转发会从内网主机主动连接 VPS,再让 VPS 上的监听端口回到内网服务。整个过程只要求内网主机能够访问互联网,不要求家庭网络拥有公网地址。

flowchart LR
  C[外部电脑] -->|SSH ProxyJump| V[VPS 公网 22]
  V -->|127.0.0.1:2022| T[SSH 反向隧道]
  T -->|127.0.0.1:22| H[家中 Linux 主机]
  H -->|主动建立连接| V

下面以”把家中主机的 22 端口转发到 VPS 的 127.0.0.1:2022”为例。监听地址刻意使用回环地址,避免把转发端口直接暴露到公网。

3.1 准备 VPS

任何能够稳定运行 OpenSSH、拥有公网 IP 的 VPS 都可以。先完成系统更新、普通管理员账户和防火墙配置;更完整的基础加固可以参考云服务器安全配置

以 RHEL、Fedora 或兼容发行版为例:

sudo dnf install -y openssh-server
sudo systemctl enable --now sshd
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload

云厂商若还有安全组,只需要先放行 VPS 的 SSH 端口。不需要为 2022 添加公网规则,因为它只在 VPS 的回环地址上监听。

3.2 创建专用隧道用户

不要让隧道长期使用 VPS 的 root 或日常管理员账户:

sudo useradd --create-home tunnel
sudo passwd tunnel

在家中主机上生成一把专用 Ed25519 密钥,并复制到 VPS:

ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_tunnel
ssh-copy-id -i ~/.ssh/id_ed25519_tunnel.pub [email protected]

首次连接时应通过 VPS 控制台或云厂商提供的信息人工核对主机指纹。确认专用密钥可以认证后,立即在 VPS 上锁定 tunnel 用户的密码;锁定密码不会影响公钥认证:

ssh -NT -i ~/.ssh/id_ed25519_tunnel [email protected]
# 在另一个 VPS 管理会话中执行:
sudo passwd --lock tunnel

第一条测试命令会保持连接,确认没有认证错误后按 Ctrl+C 退出。

3.3 限制远程转发权限

在 VPS 上创建 /etc/ssh/sshd_config.d/60-reverse-tunnel.conf

Match User tunnel
    PasswordAuthentication no
    AllowAgentForwarding no
    AllowTcpForwarding remote
    GatewayPorts no
    PermitListen 127.0.0.1:2022
    PermitTTY no
    MaxSessions 0

Match all

这些配置只允许 tunnel 用户建立远程转发,并且只能监听 127.0.0.1:2022MaxSessions 0 禁止 shell、命令和 SFTP 会话,但仍允许端口转发。与直接配置 GatewayPorts yes 相比,这样不会意外把任意转发端口暴露到 VPS 的公网网卡。各指令的准确含义可查阅 OpenSSH 的 sshd_config 手册

应用前必须先检查语法:

sudo sshd -t
sudo systemctl reload sshd

3.4 建立并验证隧道

在家中主机执行:

ssh -NT \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -i ~/.ssh/id_ed25519_tunnel \
  -R 127.0.0.1:2022:127.0.0.1:22 \
  [email protected]
  • -N 表示不执行远程命令;
  • -T 表示不分配伪终端;
  • -R 建立远程端口转发;
  • ExitOnForwardFailure 确保监听失败时 SSH 立即退出;
  • ServerAliveIntervalServerAliveCountMax 用于发现失效连接。

登录 VPS 后验证监听地址:

ss -lnt | grep 2022
ssh -p 2022 [email protected]

外部电脑可以把 VPS 作为跳板,VPS 管理账户与隧道账户应分开:

ssh -J [email protected] -p 2022 [email protected]

3.5 使用 systemd 自动重连

OpenSSH 的存活检测配合 systemd 重启策略已经能可靠维护隧道,不一定需要额外安装 autossh。

在家中主机创建 /etc/systemd/system/ssh-reverse-tunnel.service。必须把用户名、主目录和域名替换为实际值;systemd 的 ExecStart 不会展开 ~

[Unit]
Description=Reverse SSH tunnel to VPS
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=home_user
ExecStart=/usr/bin/ssh -NT \
  -o BatchMode=yes \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -i /home/home_user/.ssh/id_ed25519_tunnel \
  -R 127.0.0.1:2022:127.0.0.1:22 \
  [email protected]
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

启用并查看日志:

sudo systemctl daemon-reload
sudo systemctl enable --now ssh-reverse-tunnel.service
systemctl status ssh-reverse-tunnel.service
journalctl -u ssh-reverse-tunnel.service -f

如果确实要让 VPS 的 2022 直接监听公网,才将 GatewayPorts 改为 clientspecified,并把 -R 的监听地址改为 0.0.0.0::。同时必须用 VPS 防火墙或安全组限制来源地址。对管理用 SSH,优先保留回环监听和 ProxyJump。

4. 使用 Cloudflare Tunnel

Cloudflare Tunnel 在内网运行 cloudflared,由它主动建立到 Cloudflare 的出站连接。因此家中不需要公网 IP,也不需要在路由器上开放入站端口。

flowchart LR
  U[访问者] -->|HTTPS / Access| C[Cloudflare 网络]
  C -->|已建立的 Tunnel| D[cloudflared]
  D --> W[内网 Web 服务]

公开 Web 应用和 SSH 的工作方式不同:

  • HTTP/HTTPS 可以发布为普通 hostname;
  • SSH、RDP 和任意 TCP 服务需要访问端安装 cloudflared,或通过 Cloudflare One Client 访问私有网段;
  • Cloudflare Access 可以在请求到达源站前增加身份验证和访问策略。

4.1 创建 Tunnel

公开 hostname 需要一个已接入 Cloudflare 的域名。进入 Cloudflare 控制台的 Networking → Tunnels,创建一个 cloudflared Tunnel。控制台名称可能随版本调整,以官方 Tunnel 配置说明文档为准。

创建完成后,控制台会生成一条包含 Tunnel token 的安装命令。token 等同于该 Tunnel 连接器的凭据,不要放进文章、Git 仓库、截图或容器镜像;一旦泄漏,应立即轮换。

4.2 用 Docker Compose 运行 cloudflared

创建 .env

TUNNEL_TOKEN=替换为控制台生成的真实_token

限制文件权限,并确保它已加入 .gitignore

chmod 600 .env

创建 compose.yaml

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    restart: unless-stopped
    command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN}
    extra_hosts:
      - "host.docker.internal:host-gateway"

启动并查看日志:

docker compose up -d
docker compose logs -f cloudflared

容器内的 localhost 指向容器自身,而不是 Docker 主机。源服务位于主机时,可以使用 host.docker.internal 和上面的 host-gateway 映射;源服务也是容器时,最好让两个服务加入同一 Docker 网络并使用 Compose 服务名,例如 http://grafana:3000

4.3 发布 Web 应用

打开刚创建的 Tunnel,在 Routes 中添加 Published application

Hostname: cockpit.example.com
Service URL: https://host.docker.internal:9090

如果 cloudflared 直接安装在主机而不是容器中,可以使用 https://localhost:9090。源站使用自签名证书时,优先让 cloudflared 信任正确的 CA;仅在清楚风险时,才在 Origin settings 中启用 No TLS Verify

Tunnel 只解决网络可达性。Cockpit、管理面板和内部工具还应创建 Cloudflare Access 自托管应用,并配置允许的身份、邮箱域或用户组。Access 是源站认证之前的额外防线,不能替代源站自己的账户和权限控制。

4.4 通过 Cloudflare Access 连接 SSH

在 Tunnel 的 Routes 中添加 Published application:

Hostname: ssh.example.com
Service: SSH
Service URL: localhost:22

再为 ssh.example.com 创建 Cloudflare Access 自托管应用和允许策略。访问端也要安装 cloudflared,并在 ~/.ssh/config 中加入:

Host ssh.example.com
    ProxyCommand /usr/local/bin/cloudflared access ssh --hostname %h

macOS Homebrew 等安装方式的实际路径可能不同,可以用 command -v cloudflared 查询。连接时浏览器会打开身份认证页面:

ssh [email protected]

这种方式不是把公网 22 端口直接转发到家中,而是由客户端 cloudflared 建立经过 Access 验证的连接。完整步骤可参考 Cloudflare 的客户端 cloudflared SSH 指南

如果希望像在局域网中一样访问多种非 HTTP 服务,可以向 Tunnel 添加内网 IP/CIDR route,再让授权设备通过 Cloudflare One Client 接入。该模式需要设备注册、Split Tunnel 和 Gateway 网络策略,详见私有网络路由说明文档

4.5 示例:安全发布 Cockpit

在 RHEL、Fedora 或兼容发行版安装 Cockpit:

sudo dnf install -y cockpit
sudo systemctl enable --now cockpit.socket
sudo ss -lntp | grep 9090

Cockpit 默认使用 HTTPS 9090 端口。配置 Tunnel 后,再给 hostname 添加 Cloudflare Access 策略。不要仅因为前面有 Tunnel 就使用弱密码,也不要把 9090 同时开放到公网。

通过 Cloudflare Tunnel 访问 Cockpit 登录页

5. 使用 Tailscale

Tailscale 基于 WireGuard 为设备建立私有的 tailnet。它会优先尝试设备间直连;无法直连时可以通过中继传输。无论哪种路径,都不需要在家庭路由器上开放 SSH 或 Cockpit 端口。

flowchart LR
  L[笔记本 / 手机] <-->|加密 tailnet| H[家中 Linux 主机]
  L -->|MagicDNS 名称| H
  H --> N[仅内网可见的服务]

5.1 在两端安装并加入 tailnet

Linux 可以使用官方安装脚本;不希望执行 curl | sh 时,也可以按官方软件源说明手动安装:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale status
tailscale ip

在笔记本或手机上安装 Tailscale,使用同一组织的身份登录。启用 MagicDNS 后,可以直接使用设备名访问:

tailscale ping home-server
ssh home_user@home-server

这里仍然使用系统原有的 OpenSSH 和密钥认证,只是网络流量经由 tailnet 到达目标主机。也可以访问其他端口:

https://home-server:9090
http://home-server:3000

5.2 使用统一域名访问内外网

家庭服务常遇到一个地址割裂问题:在家时使用 10.x.x.x,离开家庭网络后改用 VPN 地址或另一个反向代理域名。更简单的设计是让内外网始终使用同一个域名,并让它解析到服务的内网 IP:

immich.home.example.com -> 10.0.0.20
grafana.home.example.com -> 10.0.0.20

家庭局域网中的设备直接访问这个内网地址;外部设备加入 tailnet 后,则通过 Tailscale subnet router 到达同一个网段。Tailscale 在这里不是为每项服务提供新的 100.x.x.x 地址,而是充当一条安全的远程局域网路由。

flowchart LR
  L[家庭局域网设备] --> D[家庭 DNS]
  R[外部设备] -->|Tailscale Split DNS| D
  D -->|统一域名解析| I[内网 IP]
  L -->|局域网直连| I
  R -->|Subnet Router| I
  I --> N[Nginx / 服务]

为了让外部设备解析同一组内部域名,可以在 Tailscale 中配置 Split DNS,把 home.example.com 等内部域的查询交给家庭 DNS。这样每一层只承担一种职责:

  • DNS 将统一域名解析到内网 IP;
  • Tailscale 让外部设备能够路由到家庭内网;
  • Nginx 根据域名把请求转发到对应服务。

客户端不需要判断自己当前位于家中、公司还是移动网络,始终访问 https://immich.home.example.com 即可。Subnet Router、Split DNS 和 HTTPS 证书可以分别配置,不必为每个服务再建立一套外网地址体系。

5.3 可选:启用 Tailscale SSH

Tailscale SSH 可以使用 tailnet 身份和策略管理 SSH 认证,不必分发传统 SSH 公钥。在服务器上启用:

sudo tailscale set --ssh

生产环境不要长期沿用宽泛的默认访问规则。新的网络访问规则应使用 grants,Tailscale SSH 身份规则则放在独立的 ssh 区段。下面是一个限制到非 root SSH 的结构示例,保存前要替换用户和标签,并在管理控制台中验证策略:

{
  "groups": {
    "group:home-admins": ["[email protected]"]
  },
  "tagOwners": {
    "tag:home-server": ["group:home-admins"]
  },
  "grants": [
    {
      "src": ["group:home-admins"],
      "dst": ["tag:home-server"],
      "ip": ["tcp:22", "tcp:9090"]
    }
  ],
  "ssh": [
    {
      "action": "check",
      "src": ["group:home-admins"],
      "dst": ["tag:home-server"],
      "users": ["autogroup:nonroot"],
      "checkPeriod": "12h"
    }
  ]
}

check 会要求用户定期重新完成身份验证。策略语法和功能会持续更新,应同时参考 GrantsTailscale SSH 官方说明文档。

还需要在 Machines 页面或注册服务器时给目标设备分配 tag:home-server;否则上面的目标选择器不会匹配该服务器。

5.4 访问不能安装 Tailscale 的内网设备

打印机、NAS、摄像头等设备可能无法安装客户端。这时可以让一台常开的 Linux 主机充当 subnet router。

先启用转发:

sudo sh -c 'printf "%s\n" "net.ipv4.ip_forward = 1" "net.ipv6.conf.all.forwarding = 1" > /etc/sysctl.d/99-tailscale.conf'
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

再发布真实的家庭网段:

sudo tailscale set --advertise-routes=192.168.50.0/24

到 Tailscale 管理控制台的 Machines 页面批准该 route,并用 grants 限制哪些身份可以访问它。Linux 客户端还要显式接受子网路由:

sudo tailscale set --accept-routes

客户端所在网络不能与家庭网段重叠;例如两边都使用 192.168.1.0/24 时,路由会产生歧义。完整配置与高可用方案见 Subnet routers 说明文档。

6. 常见故障排查

IPv6 能出站但不能入站

依次检查运营商是否允许入站、光猫/路由器 IPv6 防火墙、主机防火墙、服务是否监听 [::],以及 DNS AAAA 是否仍指向当前地址。不要用同一家庭 Wi-Fi 作为唯一测试环境。

SSH 反向隧道启动失败

在家中主机使用详细日志测试:

ssh -vvv -NT -R 127.0.0.1:2022:127.0.0.1:22 [email protected]

在 VPS 检查 sshd -tjournalctl -u sshdss -lntp。常见原因包括 PermitListen 不匹配、端口已占用、主机指纹未写入运行用户的 known_hosts,或 systemd 单元中错误使用了 ~

Cloudflare Tunnel 显示健康但返回 502

健康只表示 cloudflared 已连接 Cloudflare,不表示它能访问源服务。在 cloudflared 所在主机或容器中测试 Service URL,重点检查协议、端口、TLS 证书和 Docker 网络。容器中的 localhost 几乎总是最先要排查的地方。

Tailscale 名称无法解析或连接被拒绝

tailscale status
tailscale ping home-server
tailscale netcheck

名称问题检查 MagicDNS;网络拒绝检查 grants、设备标签与目标服务监听地址;Tailscale SSH 还要检查独立的 ssh 策略。subnet router 场景则同时检查 route 是否已批准,以及 Linux 客户端是否接受路由。

7. 总结

四种方案解决的是不同问题:

  • Tailscale 最适合授权设备之间的日常远程访问;
  • Cloudflare Tunnel 最适合带域名、身份验证和边缘防护的 Web 应用;
  • 公网 IPv6 路径最直接,但安全责任也完全落在主机和路由器上;
  • SSH 反向隧道 灵活且可自控,代价是持续维护 VPS 和隧道。

无论选择哪一种,都应坚持最小暴露面、密钥或身份认证、最小权限和可审计日志。先建立私有访问,再决定哪些服务确实需要公开,通常比”先开端口再加固”更安全。