阿里云低成本容器化服务器群的网关基础设施。一台有公网 IP 的 ECS 作为中转机,同时承担三重身份:SNAT 出口网关 + NPM 反向代理入口 + 容器管理面(Dockge / Filebrowser)。内网业务 ECS 无需公网 IP,通过本套 infra 完成对外联网与对外服务发布。
外部访问链路为 Internet → 阿里云 ESA → 中转机 NPM → 内网业务 ECS;出站链路为 内网 ECS → VPC 路由 → 中转机 iptables SNAT → Internet。这种拓扑让内网机完全隐藏在中转机与 ESA 之后,只暴露一个入口,形成一个 "七层负载均衡型安全网关"。
| 层级 | 组件 | 职责 | 对应配置 |
|---|---|---|---|
| 边缘层 | 阿里云 ESA | CDN / WAF / 隐藏源站真实 IP | 阿里云控制台(不在本仓库) |
| 网关层(中转机) | iptables SNAT | 内网 → 公网 出站转发(MASQUERADE) | setup_snat.sh |
| 网关层(中转机) | Nginx Proxy Manager | 公网 → 内网 反向代理、SSL 卸载 | infra/docker-compose.yml 中 npm |
| 网关层(中转机) | Dockge | Compose 栈管理 UI(仅内部,经 NPM 反代) | 同上 dockge |
| 网关层(中转机) | Filebrowser | 网页版文件管理(仅内部,经 NPM 反代) | 同上 filebrowser |
| 内网层 | 业务 ECS | 无公网 IP,只有私网地址 | 各自业务仓库 |
flowchart LR
Internet([Internet])
ESA[阿里云 ESA<br/>CDN / WAF]
subgraph Gateway[中转机 ECS · 公网 IP]
direction TB
IPT{{iptables SNAT<br/>MASQUERADE}}
subgraph Net[Docker 网络: npm_shared_net]
direction TB
NPM[NPM<br/>:80 / :443]
Dockge[Dockge<br/>:5001 内网]
FB[Filebrowser<br/>:80 内网]
end
end
subgraph VPC[同 VPC 内网 ECS · 无公网 IP]
direction TB
SvcA[业务服务 A]
SvcB[业务服务 B]
end
Internet -- HTTPS --> ESA
ESA -- 回源 --> NPM
NPM -- 反代 --> SvcA
NPM -- 反代 --> SvcB
NPM -- 反代 --> Dockge
NPM -- 反代 --> FB
SvcA -. 出站 .-> IPT
SvcB -. 出站 .-> IPT
IPT -. 公网 .-> Internet
入站(外部访问内网业务)
- 用户请求
https://app.example.com→ 命中 ESA 站点; - ESA 回源到中转机公网 IP:443(或 80);
- 中转机上的 NPM 容器按域名匹配 Proxy Host,反代到
http://<内网机IP>:<业务端口>; - 业务返回,响应沿原路回到客户端。
出站(内网机访问公网)
- 内网机发起
ping 8.8.8.8,源 IP 为其私网地址; - VPC 路由表将
0.0.0.0/0下一跳指向中转机,包进入中转机eth0; - 中转机内核
ip_forward=1,POSTROUTING 的MASQUERADE将源 IP 改写为中转机的出口 IP; - 回包到达中转机后被 conntrack 还原为内网机私网地址并转发回去。
可在中转机 sudo tcpdump -n -i any icmp 观察到"四步走"的抓包序列——这是整条链路打通的判据。
- 中转机 ECS:1 台,有公网 IP,操作系统为 Ubuntu(推荐 24.04 LTS)或兼容 Ubuntu 的阿里云 Linux 发行版,建议开启 BBR。
- 内网机 ECS:若干,与中转机同一 VPC,无公网 IP。
- 阿里云 ESA:可选但强烈推荐,用于隐藏中转机公网 IP 与边缘防护。
- 域名:已解析接入 ESA(或直接指向中转机)。
- 内网规划:本仓库默认内网 CIDR 为
192.168.0.0/16;如不同需同步修改 setup_snat.sh 的第 5 步规则。
这一步是云环境自建网关最容易漏的部分,必须在跑脚本之前完成,否则包进不来也出不去。
- 确认中转机与所有内网机在同一 VPC,且内网机所在 VSwitch 与中转机互通。
- 关闭中转机网卡的"源/目的检查":ECS 控制台 → 实例 → 网卡 → 找到中转机网卡 → 修改"源/目的检查"为关闭。云底层默认会丢弃所有目标 IP 非网卡自身的包,不关掉 SNAT 无从谈起。
- VPC 路由表:VPC 控制台 → 路由表 → 选中内网机所在 VSwitch 关联的路由表 → 添加自定义路由条目:
- 目标网段:
0.0.0.0/0 - 下一跳类型:ECS 实例
- 下一跳实例:选择中转机
- 目标网段:
- 中转机安全组:
- 入方向:允许来源
192.168.0.0/16(或你的实际内网段)全部协议、全部端口; - 入方向:允许公网 TCP
80、443(供 ESA 回源或直接访问); - 入方向:临时允许管理 IP 访问 TCP
81(NPM 初始化后回收); - 出方向:默认放行即可。
- 入方向:允许来源
将 setup_snat.sh 上传至中转机,执行:
chmod +x setup_snat.sh
sudo ./setup_snat.sh脚本做的事(对应文件内的 9 步):
- 开启
net.ipv4.ip_forward=1并写入/etc/sysctl.d/99-snat.conf持久化;同时关闭rp_filter避免多网卡下反向路径过滤误杀。 - 安装
iptables-persistent(仅 apt 系发行版,Ubuntu / 兼容 Ubuntu 的阿里云 Linux 均适用)。 - 清理
filter / nat / mangle三表旧规则,防止重复执行叠加。 FORWARD链默认策略改为DROP,锁死默认转发。- 放行源地址为
192.168.0.0/16的转发包(内网出站白名单)。 - 放行
ESTABLISHED,RELATED状态包(保证回程包能回来)。 POSTROUTING上对-o eth0的包做MASQUERADE,完成 SNAT。mangle表的TCPMSS --clamp-mss-to-pmtu解决 VPC 隧道 MTU 导致的大包丢包 / HTTPS 卡顿。iptables-save > /etc/iptables/rules.v4持久化。
若中转机出口网卡不是
eth0,先用ip route show default确认,然后同步修改脚本第 7 步。
在中转机上验证:
sysctl net.ipv4.ip_forward # => net.ipv4.ip_forward = 1
sudo iptables -t nat -L -n -v # POSTROUTING 链应看到 MASQUERADE
sudo iptables -L FORWARD -n -v # 应看到 192.168.0.0/16 ACCEPT 与 ESTABLISHED,RELATED ACCEPT在内网机上验证:
ping 8.8.8.8 # 应通
ping www.baidu.com # 若解析不出 IP,见 FAQ 2
curl -I https://www.google.com # HTTPS 也应通参照 Docker 官方文档 安装 Docker Engine 与 Compose 插件。装完后:
docker network create npm_shared_netinfra/docker-compose.yml 把
npm_shared_net声明为external: true。若不先手工创建,docker compose up会直接报错退出。
cd infra
docker compose up -d
docker compose ps三个容器应全部处于 running 状态:
npm:对外暴露80(443与81在 yml 中被注释,按需开);dockge:仅内部,通过共享网络由 NPM 反代;filebrowser:仅内部,同上。
数据持久化在以下目录(首次启动自动生成):
infra/
├── npm/data/ # NPM 配置、数据库、Proxy Host 定义
├── npm/letsencrypt/ # Let's Encrypt 证书
├── dockge/data/ # Dockge 元数据
├── dockge/stacks/ # Dockge 管理的额外 compose 栈
└── filebrowser/
├── config/
└── database/
NPM 的管理后台在 81 端口。仓库默认已将其在 yml 中注释掉以减小攻击面,初始化时需临时暴露:
- 编辑 infra/docker-compose.yml,取消
- "81:81"这一行前的#;同时取消- "443:443"注释以供正式对外服务。 docker compose up -d让变更生效。- 浏览器访问
http://<中转机公网IP>:81,默认账号admin@example.com/ 密码changeme,登录后立即修改邮箱和密码。 - 在 NPM 后台 →
Hosts→Proxy Hosts→Add Proxy Host:- Dockge:Domain
dockge.example.com,Schemehttp,Forward Hostnamedockge,Forward Port5001,勾选Block Common Exploits、Websockets Support。 - Filebrowser:Domain
files.example.com,Schemehttp,Forward Hostnamefilebrowser,Forward Port80。 - 业务服务:Domain
app.example.com,Schemehttp,Forward Hostname<内网机私网 IP>,Forward Port<业务端口>。
- Dockge:Domain
- 对每个 Host 的
SSL标签页选择Request a new SSL Certificate(Let's Encrypt),勾选Force SSL与HTTP/2 Support,可选开启HSTS。
Forward Hostname 用容器名(
dockge/filebrowser)能通的前提是它们都加入了npm_shared_net这张网络,Docker 的内嵌 DNS 会自动解析。
初始化完成后务必回收管理面:
# 重新注释掉 docker-compose.yml 里的 - "81:81"
docker compose up -d随后通过一个专门的 npm.example.com Proxy Host 指向 http://npm:81 来访问后台,既能走 HTTPS 也能借助 ESA 的访问控制。
- 阿里云 ESA → 接入网站 → 填入你的域名。
- 源站配置:IP 或域名指向中转机公网 IP,端口
443(推荐开启回源 TLS)或80。 - 按 ESA 控制台提示,将域名 DNS 切换到 ESA 提供的 CNAME / NS。
- 开启 WAF 基础防护、速率限制、Bot 管理等策略。
- 切流后外部流量首先打到 ESA,中转机真实 IP 对外不可见。
- 出站:任一内网机
curl -I https://www.google.com返回 200/301/302 即表示 SNAT 通。 - 入站:浏览器访问
https://app.example.com,经 ESA → NPM → 内网业务返回页面。 - 链路抓包:中转机执行
sudo tcpdump -n -i any icmp,内网机ping 8.8.8.8,应看到如下"四步走":
eth0 In IP 192.168.x.x > 8.8.8.8: ICMP echo request
eth0 Out IP <中转机私网IP> > 8.8.8.8: ICMP echo request # SNAT 改写源地址
eth0 In IP 8.8.8.8 > <中转机私网IP>: ICMP echo reply
eth0 Out IP 8.8.8.8 > 192.168.x.x: ICMP echo reply # conntrack 还原
若只看到前 1 条或前 2 条,参考下方 FAQ。
- SPOF 提示:中转机挂掉 → 内网全部失联。请在阿里云云监控为中转机配置 CPU / 带宽 / 磁盘 / 网络包量的告警。
- BBR:中转机与内网机都建议开启,能显著改善转发与对外 TCP 的吞吐。
- 管理面最小暴露:
81(NPM 后台)、5001(Dockge)、Filebrowser 端口一律不直接绑定到宿主机,只通过 NPM 反代以带 SSL 的方式对外。 - iptables 持久化:脚本已把当前规则写入
/etc/iptables/rules.v4,重启后由iptables-persistent自动加载。后续若手工改了规则,记得再跑一遍sudo iptables-save > /etc/iptables/rules.v4。 - 内网机 DNS:无公网 IP 的机器不会自动拿到公网 DNS,建议在
/etc/resolv.conf或 systemd-resolved 里写223.5.5.5与8.8.8.8。 - 安全组收敛:部署完成后,把只为初始化开过的
81端口入方向规则删除。
- 现象:
tcpdump在中转机能抓到 ICMP echo request 进来,但没有 Out。 - 根因:
FORWARD链默认策略为DROP,且白名单规则没生效(常见于 Ubuntu 24.04 默认 nftables 后端、或脚本没真正执行到第 4~6 步)。 - 修复:确认 setup_snat.sh 已完整执行,或临时测试:
sudo iptables -P FORWARD ACCEPT # 临时全开确认问题 sudo iptables -F FORWARD # 清空再让脚本重建 sudo ./setup_snat.sh # 重新应用正式规则
- 现象:IP 通,域名解析失败。
- 根因:
systemd-resolved配置里 DNS 为空。 - 修复:
sudo sed -i 's/#DNS=/DNS=223.5.5.5 8.8.8.8/' /etc/systemd/resolved.conf sudo systemctl restart systemd-resolved
- 现象:DNS 能走通(UDP 53),但 ICMP 不通。
- 根因:早期版本脚本遗漏
FORWARD ACCEPT逻辑。本仓库当前 setup_snat.sh 已包含白名单 + ESTABLISHED,RELATED 两条放行规则,重跑脚本即可。
- 现象:包进中转机后消失。
- 根因:要么被
FORWARD链拦截,要么MASQUERADE没绑到正确的出口网卡。 - 修复:
ip route show default # 确认出口网卡 sudo iptables -t nat -F POSTROUTING sudo iptables -t nat -A POSTROUTING -o <出口网卡> -j MASQUERADE sudo iptables-save > /etc/iptables/rules.v4
- 现象:SNAT 明明通了,但 apt 取不到包。
- 根因:阿里云内网镜像站与内网机本地索引同步不一致,旧包被删除而新索引还没拉下来。
- 修复(优先清缓存):
sudo apt clean sudo apt update --fix-missing
- 修复(换公网源,Ubuntu 24.04 及以上的 DEB822 格式):
sudo cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak sudo sed -i 's/mirrors.cloud.aliyuncs.com/mirrors.aliyun.com/g' /etc/apt/sources.list.d/ubuntu.sources sudo apt update
- 现象:脚本、Docker、NPM 都正常,但内网机就是不通外网。
- 根因:云端未关闭"源/目的检查",或 VPC 路由表的
0.0.0.0/0下一跳没绑到中转机,或该路由表没关联到内网机所在 VSwitch。 - 修复:回 §3.1 三项逐条核对。
.
├── README.md # 本文件
├── setup_snat.sh # 中转机 SNAT 一键配置脚本
└── infra/
└── docker-compose.yml # NPM + Dockge + Filebrowser