家里的 Linux 主机可以运行 Cockpit、开发环境、监控面板和文件服务,但家庭宽带往往没有公网 IPv4。所谓”内网穿透”,本质上是为外部设备建立一条能够到达内网服务的路径。
这篇文章介绍四种常见方案:
- 直接使用运营商分配的公网 IPv6;
- 通过有公网 IP 的 VPS 建立 SSH 反向隧道;
- 使用 Cloudflare Tunnel 发布 Web 或 SSH 服务;
- 使用 Tailscale 组建只对授权设备开放的私有网络。
**安全原则:**远程访问不等于把服务暴露给整个互联网。SSH、Cockpit、数据库和管理面板应优先使用私有网络或身份认证;只有确实需要公开访问的 Web 服务,才应配置公开域名。
1. 先选择合适的方案
| 方案 | 额外条件 | 最适合 | 是否公开服务端口 | 主要限制 |
|---|---|---|---|---|
| 公网 IPv6 | 运营商提供可入站的 IPv6 | 自己管理的 SSH、HTTPS 服务 | 是 | 地址可能变化,路由器和主机防火墙必须正确配置 |
| SSH 反向隧道 | 一台有公网 IP 的 VPS | 临时转发、完全自控的运维通道 | VPS 至少开放 SSH | 需要维护 VPS、密钥和长连接 |
| Cloudflare Tunnel | Cloudflare 账户;公开 hostname 还需要托管域名 | 对外发布 Web 应用,或通过 Access 访问 SSH | 否 | SSH 等非 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:2022;MaxSessions 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 立即退出;ServerAliveInterval和ServerAliveCountMax用于发现失效连接。
登录 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 同时开放到公网。

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 会要求用户定期重新完成身份验证。策略语法和功能会持续更新,应同时参考 Grants 与 Tailscale 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 -t、journalctl -u sshd 和 ss -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 和隧道。
无论选择哪一种,都应坚持最小暴露面、密钥或身份认证、最小权限和可审计日志。先建立私有访问,再决定哪些服务确实需要公开,通常比”先开端口再加固”更安全。