用 frp + Nginx + Cloudflare 搭建内网穿透:把内网网站安全地暴露到公网


一、安装 frps

查最新版本(本文使用 0.69.1):https://github.com/fatedier/frp/releases

1
2
3
4
5
6
7
8
9
10
11
12
# 下载并解压(按需替换版本号和架构,这里是 linux amd64)
cd /tmp
VER="0.69.1"
wget https://github.com/fatedier/frp/releases/download/v${VER}/frp_${VER}_linux_amd64.tar.gz
tar -xzf frp_${VER}_linux_amd64.tar.gz

# 安装 frps 二进制
install -m 755 frp_${VER}_linux_amd64/frps /usr/local/bin/frps
frps --version

# 配置目录
mkdir -p /etc/frp

二、配置 frps:端口、鉴权、子域名

先生成一个随机鉴权 Token(这是客户端连接 frps 的凭证,务必保密):

1
2
3
TOKEN=$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 32)
echo "$TOKEN" # 记下来,客户端要用
echo "$TOKEN" > /etc/frp/.frps_token

写入 /etc/frp/frps.toml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# frpc 客户端连接端口(直连 IP:7000,不走 Cloudflare)
bindPort = 7000

# HTTP vhost 入口端口:仅本机,Nginx 会把 443 流量转到这里
# 选一个没被占用的端口(示例 7080),不要放行到公网
vhostHTTPPort = 7080

# 子域名宿主:客户端 subdomain="app1" → app1.example.com
subdomainHost = "example.com"

# 鉴权
auth.method = "token"
auth.token = "<把上面生成的 TOKEN 填这里>"

# 日志
log.to = "/var/log/frps.log"
log.level = "info"
log.maxDays = 3

三个端口分别是什么

端口 作用 是否对公网开放
7000 frpc 客户端连接用,frpc 直连 YOUR_SERVER_IP:7000 (ufw 放行)
7080 HTTP vhost 入口,Nginx 从本机转发过来 (仅 127.0.0.1,不放行)
7500 管理面板(可选) 本文不启用

为什么 7000 不能套 Cloudflare 小黄云?
Cloudflare 只代理 HTTP/HTTPS 的特定端口(80/443/8080/8443 等),7000 不在内;而且 frp 跑的是自有二进制协议,不是 HTTP,Cloudflare 根本承载不了。所以 frpc 必须直连源站
**** IP:7000
——这是 frp 架构的前提。客户端配置里写 IP 是私有的,不等于把 IP 放进公共 DNS 泄露(后者才是要避免的)。


三、systemd 服务 + 开机自启 + 防火墙

创建 /etc/systemd/system/frps.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=frp server (frps)
After=network.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

启用并放行端口:

1
2
3
4
5
systemctl daemon-reload
systemctl enable --now frps # 启动 + 开机自启

ufw allow 7000/tcp # 只放行客户端连接端口
# 注意:不要放行 7080(它是内部 vhost 端口)

验证:

1
2
systemctl is-active frps             # active
ss -tlnp | grep frps # 应监听 7000 + 7080

四、配置 Nginx:把 443 流量反代到 frps vhost

frps 的 vhost 端口(7080)跑的是明文 HTTP,我们用 Nginx 在前面终结 TLS,再把流量(保留 Host 头)转给 frps。frps 靠 Host 头来路由到对应子域名的客户端。

在 Nginx 配置里(如 /etc/nginx/conf.d/your-site.conf)加一个通配 server 块:

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
# WebSocket 支持(map 段放 http 块顶部,全局一次即可)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# 80 → 跳转 https
server {
listen 80;
listen [::]:80;
server_name *.example.com;
return 301 https://$host$request_uri;
}

# 443 → frps vhost(7080)
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name *.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;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;

location / {
proxy_pass http://127.0.0.1:7080;
proxy_http_version 1.1;
proxy_read_timeout 300s;
proxy_send_timeout 300s;

# 关键:把原始 Host 传给 frps,它靠这个路由
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
}

通配块 vs 精确块:Nginx 的 server_name 匹配,精确名称优先于通配符。所以你已有的精确子域名站点(如 blog.example.com
走自己的后端)不受影响;只有没单独定义的子域名才会落进这个通配块 → frps。

这就是”新增穿透站不用动 Nginx”的关键:一个通配块把所有 *.example.com 都接管了。

生效:

1
nginx -t && systemctl reload nginx

此时本地测试链路(还没客户端,frps 会返回 404,属正常,证明管道通了):

1
curl -H "Host: app1.example.com" http://127.0.0.1:7080/   # 应返回 frp 的 404

五、配置 Cloudflare 子域名(隐藏源站 IP)

frp 的 subdomainHost 让客户端用 subdomain = "app1" 自动拼成 app1.example.com,但 DNS 还是要一条一条加

正确做法:每个子域名加一条「橙云」A 记录

在 Cloudflare → DNS 里,为每个要用作穿透的子域名加:

类型 名称 内容 代理状态
A app1 YOUR_SERVER_IP 已代理(橙云)
A app2 YOUR_SERVER_IP 已代理(橙云)

橙云 = Cloudflare 代理:对外只显示 Cloudflare 的 IP,源站真实 IP 被隐藏,还白嫖了 TLS 和 DDoS 防护。

千万别加「灰云通配记录」

新手很容易想偷懒,加一条灰云(DNS only)的 *.example.com → YOUR_SERVER_IPCloudflare 免费套餐无法代理通配记录,它只能是灰云——于是任何人 dig anything.example.com
**** 都能直接拿到你的源站真实 IP
,Cloudflare 的保护形同虚设。一旦源站 IP 暴露,攻击者可以绕过 Cloudflare 直接打你源站,殃及你所有服务。

简单记:橙云逐条加 = 隐藏 IP;灰云通配 = 暴露 IP。多花几秒钟点几下,值得。


六、客户端 frpc 配置

内网机器上(你的电脑、家里 NAS 等),下载同版本的 frpc,写入 frpc.toml:

1
2
3
4
5
6
7
8
9
10
11
serverAddr = "YOUR_SERVER_IP"     # 直连 IP,7000 不走 Cloudflare
serverPort = 7000
auth.method = "token"
auth.token = "<和服务端一致的 TOKEN>"

[[proxies]]
name = "app1"
type = "http"
localIP = "127.0.0.1"
localPort = 8080 # 本地服务的真实端口
subdomain = "app1" # → https://app1.example.com

启动:

1
2
./frpc -c frpc.toml
# 看到 "login to server success" 就连上了

访问 https://app1.example.com,就是你的内网网站了。

⚠️ 一个真实踩坑:macOS / IPv6 的 localhost

如果在 macOS 上跑 frpc,本地服务用 localhost 能访问,但 frp 报 “The page … currently unavailable. The server is powered by frp.”,服务端日志出现 do http proxy request error: EOF——

原因:macOS 的 localhost 优先解析到 IPv6 ::1,很多开发服务器只绑 ::1、不绑 IPv4 127.0.0.1。而 frpc 配的是 localIP = "127.0.0.1"(IPv4),连不上。

解决:把 frpc 的 localIP 改成 ::1:

1
localIP = "::1"      # 本地服务监听 IPv6 时用这个;监听 IPv4 才用 127.0.0.1

验证方法:curl http://127.0.0.1:端口 失败但 curl http://localhost:端口 正常 = 服务只在 IPv6 上。frpc 里写 ::1 即可。


七、新增一个穿透站(服务端零改动)

这是这套架构最爽的地方。要再加一个站 app2.example.com:

  1. Cloudflare:加一条橙云 A 记录 app2 → YOUR_SERVER_IP
  2. 客户端 frpc.toml 加一段:
1
2
3
4
5
6
[[proxies]]
name = "app2"
type = "http"
localIP = "127.0.0.1"
localPort = 3000
subdomain = "app2"
  1. 客户端重启 frpc。

服务端(frp 配置、Nginx、防火墙、证书)一概不动。 因为 subdomainHost + Nginx 通配块已经就绪。


八、安全与运维提示

  • Token 是命门:泄露 = 任何人都能挂代理白嫖你的服务器做中转。务必保密,定期换。
  • 面板能不开就不开:frps 的 web 面板(webServer.port = 7500)默认只读监控,没强需求就别开,少一个暴露面。
  • 想加密 frpc**↔**frps 隧道:服务端 transport.tls.force = true,客户端 transport.tls.enable = true,防止 token 和流量被嗅探。
  • SSH/RDP 这类不想暴露公网端口的:用 type = "stcp"(密钥点对点),服务端一个额外端口都不用开,比 TCP 转发安全得多。
  • 日常命令:
1
2
3
4
systemctl status frps          # 状态
systemctl restart frps # 改配置后重启
journalctl -u frps -f # 实时日志
ss -tlnp | grep frps # 监听端口

小结

整套方案的核心是三层分工:

组件 职责
入口/安全 Cloudflare 橙云 隐藏 IP、TLS、防 DDoS
TLS/路由 Nginx 通配块 终结 TLS,按域名转发
穿透 frps + subdomainHost 动态路由到各客户端

配置好之后,日常加站就是「DNS 加一条 + 客户端加一段」两步,服务端永远是透明的。希望对你有帮助。