国内云厂商对未备案域名的拦截集中在 80/443,非标端口通常不受影响。将 Cloudflare 设为 DNS Only(灰云),让浏览器直连源站的非标端口,再用 DNS-01 签发 Let’s Encrypt 证书,即可在未备案的情况下提供可信 HTTPS。

DNS Only 不是 Cloudflare 反代:橙云(Proxied)才会转发流量,灰云只负责解析。

按应用位置分为两种形态: - 形态 A:应用与 Nginx 都在云服务器上。 - 形态 B:应用在 NAT 后的内网机器上,通过 frp 穿透。frp 基础可参考《frp内网穿透搭建教程》。

两种形态的证书与公网 Nginx 配置基本相同,操作顺序为:接入 Cloudflare → 签发证书 → 配置 Nginx(形态 B 再配 frp)→ 验证 → 确认 DNS。

一、为什么选择 DNS Only?

橙云(Proxied) 灰云(DNS Only,本文)
浏览器连接 Cloudflare 边缘 直连源站
HTTPS 端口 443、2053、2083、2087、2096、8443 任意端口
源站证书 Origin CA 或自签即可 必须是浏览器信任的证书
源站 IP 隐藏 公开
上传、流式输出、WebSocket 受 Cloudflare 限制 由源站自行控制
国内访问 延迟可能波动 通常更稳定

灰云适合需要任意端口、流式传输或端到端控制的场景;代价是源站 IP 暴露,需要自行配置证书和防火墙。

二、目标链路

形态 A:应用在云服务器

以 https://api.example.com:8443/ 为例:

1
2
3
4
5
6
7
8
9
10
11
flowchart TD
U["浏览器<br/>https://api.example.com:8443/"]
DNS["Cloudflare DNS(灰云)"]
subgraph ORIGIN["云服务器"]
NGINX["Nginx :8443<br/>终结 TLS"]
APP["应用 · 127.0.0.1:3000"]
end
U --> DNS
DNS --> U
U -- "HTTPS" --> NGINX
NGINX -- "明文 HTTP" --> APP

形态 B:应用在内网机器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
flowchart TD
U["浏览器<br/>https://a.example.com:8443/"]
DNS["Cloudflare DNS(灰云)"]
subgraph VPS["云服务器"]
NGINX["Nginx :8443<br/>终结 TLS,保留 Host"]
FRPS["frps<br/>控制端口 27500<br/>代理端口 127.0.0.1:27022"]
end
subgraph LAN["内网机器(无公网 IP)"]
FRPC["frpc<br/>主动外连 VPS:27500"]
NGLAN["Nginx :8899<br/>按 Host 分发"]
APP1["应用 A · :3000"]
APP2["应用 B · :8000(WebSocket)"]
end
U --> DNS
DNS --> U
U -- "HTTPS" --> NGINX
NGINX -- "回环 HTTP" --> FRPS
FRPS <-- "frp 隧道(TLS)" --> FRPC
FRPC --> NGLAN
NGLAN --> APP1
NGLAN --> APP2

Nginx 到 frps 的 HTTP 仅走本机回环,frpc 到 frps 的隧道由 frp TLS 加密。

三、准备

先在当前终端设置变量(替换为自己的值),后文命令会自动展开 $DOMAIN 等变量;变量仅在当前终端有效,新窗口需重新设置。

1
2
3
4
5
DOMAIN=example.com          # 主域名;通配符证书覆盖所有一级子域
ORIGIN_IP=203.0.113.10 # 服务器公网 IP(示例地址,必须替换)
HTTPS_PORT=8443 # 对外 HTTPS 端口
APP_PORT=3000 # 形态 A:本地应用端口
FRP_PORT=27500 # 形态 B:frps 控制端口

1. 接入 Cloudflare 并配置解析

打开 Cloudflare 控制台,通过 Add a site 添加主域名,再按提示在域名注册商处把 NS 修改为 Cloudflare 分配的两个服务器(官方接入教程)。等待 Zone 状态变为 Active,这是后续 DNS-01 签发证书的前提。

进入 DNS → Records,添加需要访问的子域记录(记录管理文档):

类型 名称 IPv4 地址 代理状态 用途
A api ${ORIGIN_IP} DNS Only 形态 A
A a ${ORIGIN_IP} DNS Only 形态 B 应用 A
A c ${ORIGIN_IP} DNS Only 形态 B WebSocket 应用

TTL 可先使用 Auto。新部署可直接使用灰云;已有橙云记录可暂时保留,等第八节再切换。每个实际访问的子域都需要解析记录,通配符证书不会自动创建 DNS 记录。记录只填写 IP,不包含 :8443;没有公网 IPv6 就不要添加 AAAA。

2. 安装依赖

1
2
sudo apt update
sudo apt install -y nginx certbot python3-certbot-dns-cloudflare

3. 配置安全组

安全组与防火墙按以下范围配置:

端口 用途 开放范围
8443/tcp Nginx HTTPS 0.0.0.0/0
27500/tcp frps 控制通道(形态 B) 仅 frpc 出口 IP;无法固定时,依靠强 token 与 TLS
27022/tcp frps 代理端口(形态 B) 不对公网开放,并配置 proxyBindAddr
80 / 443 本方案不使用 无需放行

frpc 主动外连,因此内网机器无需入站规则。若使用公网 IPv6,需同步配置 Nginx 监听与 IPv6 防火墙。

四、签发通配符证书

一张证书可覆盖主域和所有一级子域,后续新增子域无需重签。

1. 创建 Cloudflare API Token

在 Cloudflare API Tokens 页面选择 Create Token,使用 Edit zone DNS 模板,仅授予目标 Zone 的 DNS: Edit 权限,不要使用 Global API Key。

创建凭据文件(续期仍需使用,注意保密且不要提交到仓库):

1
2
3
sudo install -d -m 700 /root/.secrets/certbot
sudo install -m 600 /dev/null /root/.secrets/certbot/cloudflare.ini
sudoedit /root/.secrets/certbot/cloudflare.ini

写入:

1
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN

2. 签发

1
2
3
4
5
6
7
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
--dns-cloudflare-propagation-seconds 60 \
--cert-name "$DOMAIN" \
-d "$DOMAIN" -d "*.${DOMAIN}" \
--email admin@example.com --agree-tos --non-interactive

关键参数:

  • $DOMAIN 是第三节定义的变量,执行时会展开为 example.com;${DOMAIN} 与其等价,花括号用于明确变量边界。
  • --cert-name 指定证书名称,产物位于 /etc/letsencrypt/live/${DOMAIN}/,Nginx 和续期均使用此名称。
  • 两个 -d 分别覆盖 example.com 和 *.example.com,后者仅包含一级子域,不含 a.b.example.com。
  • DNS-01 仅验证 DNS,不占用 80/443,证书可在橙云状态下签发;等待 60 秒是为了给 DNS 传播留时间。

五、配置公网 Nginx

两种形态的 TLS 与转发头配置相同,但 server_name 和上游地址不同。下面分别给出形态 A、B 的公网 server 块,按实际部署选用;形态 B 不能只修改形态 A 的 proxy_pass。

/etc/nginx/nginx.conf 会通过 include /etc/nginx/conf.d/*.conf; 加载该目录下的全部配置。文件名和拆分方式不限,可按站点分别创建文件;以下最小示例也可放入同一文件。

1
sudoedit /etc/nginx/conf.d/tunnel.conf
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
# WebSocket:避免给普通请求硬编码 Connection "upgrade"
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# 可选加固:拒绝未知 SNI(nginx >= 1.19.4)
server {
listen 8443 ssl default_server;
ssl_reject_handshake on;
}

# 形态 A:本机应用
server {
listen 8443 ssl;
server_name api.example.com;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;

client_max_body_size 100M;

location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
# 公网 Nginx 是可信代理边界,覆盖客户端提交的转发头
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;

# WebSocket,不需要可删除这两行
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

# 流式输出 / SSE / 音频流
proxy_read_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
}
}

# 形态 B:两个子域通过同一 frp 隧道,由内网 Nginx 按 Host 分发
server {
listen 8443 ssl;
server_name a.example.com c.example.com;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;

client_max_body_size 100M;

location / {
proxy_pass http://127.0.0.1:27022;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Port $server_port;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_read_timeout 3600s;
proxy_buffering off;
proxy_request_buffering off;
}
}

其他后端可复制对应形态的 server 块,并修改 server_name、proxy_pass。X-Forwarded-For $remote_addr 会丢弃客户端伪造的 XFF;X-Forwarded-Host 保留原始 Host(含 :8443),X-Forwarded-Port 单独传递端口。应用必须按框架配置可信代理,只信任来自对应 Nginx 的转发头,不能无条件信任任意客户端提供的 X-Forwarded-*。

配置完成后加载:

1
sudo nginx -t && sudo systemctl reload nginx

六、形态 B:配置 frp 隧道

1. 服务器端 frps

编辑 /etc/frp/frps.toml:

1
2
3
4
5
6
7
8
9
bindAddr = "0.0.0.0"
bindPort = 27500
auth.token = "换成强随机值,openssl rand -hex 16"

# 代理端口仅监听回环,避免直接暴露给公网
proxyBindAddr = "127.0.0.1"

# 强制 frpc 使用 TLS(frp >= 0.50)
transport.tls.force = true

创建 systemd 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sudo tee /etc/systemd/system/frps.service >/dev/null <<EOF
[Unit]
Description=FRP Server
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now frps

确认代理端口仅绑定回环地址:

1
2
3
ss -tlnp | grep frps
# 127.0.0.1:27022 # 代理端口
# 0.0.0.0:27500 # 控制端口

确认第五节使用形态 B 的 server 块后,重新加载公网 Nginx。

2. 内网机器 frpc

编辑 /etc/frp/frpc.toml:

1
2
3
4
5
6
7
8
9
10
11
serverAddr = "203.0.113.10"
serverPort = 27500
auth.token = "与 frps 一致"
transport.tls.enable = true

[[proxies]]
name = "web-tunnel"
type = "tcp"
localIP = "127.0.0.1"
localPort = 8899
remotePort = 27022

同样使用 systemd 常驻并设置 Restart=always,避免 frpc 退出导致全部站点中断。

3. 内网 Nginx 分发

编辑 /etc/nginx/conf.d/lan.conf,按域名转发到不同应用:

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
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 127.0.0.1:8899;
server_name a.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
# 透传公网 Nginx 已重建的转发头,不按 frpc 回环地址重新生成
proxy_set_header X-Real-IP $http_x_real_ip;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
}
}

server {
listen 127.0.0.1:8899;
server_name c.example.com; # WebSocket 应用
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $http_x_real_ip;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-Host $http_x_forwarded_host;
proxy_set_header X-Forwarded-Port $http_x_forwarded_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}

应用仅监听 127.0.0.1,无需开放内网机器的入站端口。内网 Nginx 也应仅通过上述可信 frp 链路接收流量;否则,直接连接它的客户端可以伪造所透传的转发头。

七、验证

请在另一台外网机器上测试,部分云环境不支持从本机访问自身公网 IP(hairpin)。以下命令通过 --resolve 绕过 DNS,直接验证源站:

1
2
3
4
5
curl --noproxy '*' --connect-timeout 10 \
--resolve "api.example.com:8443:${ORIGIN_IP}" \
-sS -o /dev/null \
-w "HTTP=%{http_code} remote=%{remote_ip} TLS=%{ssl_verify_result}\n" \
"https://api.example.com:8443/"

预期 remote 为源站 IP、TLS=0,HTTP 状态符合应用预期。不要加 -k,否则会跳过证书验证。

涉及登录或重定向时,还要检查应用日志中的客户端 IP、Location、登录回调和 Secure Cookie:外部协议应为 HTTPS/WSS,且跳转地址不能丢失 :8443。并非所有框架都会自动采用 X-Forwarded-Host 和 X-Forwarded-Port,需按所用框架启用代理感知并验证。

形态 B 再检查代理端口未暴露,并按需测试 WebSocket:

1
2
3
4
5
curl --connect-timeout 5 "http://${ORIGIN_IP}:27022/" \
&& echo "❌ 27022 暴露,检查 proxyBindAddr 和安全组" \
|| echo "✅ 27022 已隔离"

npx wscat -c "wss://c.example.com:8443/ws"

八、确认 DNS Only 并验证解析

新部署如果在第三节已创建灰云记录,无需再次切换。已有橙云记录则在 Cloudflare 控制台选择对应域名,进入 DNS → Records,将 A 记录改为 DNS only(灰云)并确认指向 ${ORIGIN_IP};需要回切时可提前调短 TTL。

如果此前使用橙云,还需关闭源站“仅允许 Cloudflare IP”的防火墙规则,以及 Authenticated Origin Pulls、Cloudflare Access 等边缘鉴权,否则直连会失败。

等待 DNS 缓存更新后,在外网机器检查解析:

1
dig +short api.example.com @1.1.1.1

灰云生效后应返回 ${ORIGIN_IP},而不是 Cloudflare 的 IP。随后即可直接访问 https://api.example.com:8443/,不再需要 --resolve。

九、自动续期

创建续期后的 Nginx 重载脚本:

1
2
3
4
5
6
7
8
9
sudo install -d -m 755 /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
nginx -t && systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

sudo systemctl enable --now certbot.timer
sudo certbot renew --dry-run --run-deploy-hooks

保留 Cloudflare 凭据供续期使用;更换 Token 后同步更新凭据文件。

十、安全清单

  • 灰云会公开源站 IP:最小化安全组放行范围,并加固 SSH。
  • Cloudflare Token 仅授予必要权限,妥善保管并定期轮换。
  • 形态 B:frp token 使用强随机值、两端一致且不进仓库;控制端口尽量限制来源 IP。
  • 形态 B:proxyBindAddr 与安全组共同隔离 27022,并显式启用 frp TLS。
  • 形态 B:frpc 是单点,配置自动重启和定期探活。
  • 为 client_max_body_size 设置明确上限,避免无限上传。

参考资料

Claude Code + 魔搭社区 API:免费白嫖新姿势

最近发现魔搭社区(ModelScope)已经支持 Anthropic API,这意味着我们可以通过一些简单的配置,让 Claude Code 直接使用魔搭社区提供的免费 API 资源。亲测可用,这里给大家分享一下具体步骤。

阅读全文 »

本文全面对比了PyInstaller和Nuitka两种Python打包工具的优劣势,帮助开发者选择最适合自己项目的打包方案。

阅读全文 »

在使用Linux系统时,我们经常会遇到一些应用需要访问国际网络资源的情况。由于网络环境的限制,这些应用可能无法正常访问所需的资源,导致功能受限或无法启动。虽然我们可以在终端中通过设置代理环境变量来解决这个问题,但每次都需要手动设置非常繁琐。本文将介绍如何通过配置应用的启动文件来永久设置代理,使应用能够自动使用代理服务。

阅读全文 »

Firefox浏览器默认使用水平标签栏布局,而Microsoft Edge的垂直侧边栏设计因其在信息密度上的优势而广受欢迎。本文将详细介绍如何在Firefox中实现类似的垂直侧边栏效果,提升您的浏览体验。

阅读全文 »

在虚拟化环境中,将物理显卡直接分配给虚拟机(即显卡直通)是提升虚拟机图形性能的重要手段。本文将详细介绍如何在Linux系统中实现KVM多显卡直通,特别适用于需要在虚拟机中获得接近原生性能的用户。

阅读全文 »
0%