Cloudflare Tunnel 将本地服务暴露至公网
使用 Cloudflared tunnel 将本机任意 HTTP 服务映射到自有域名,配套 VBS 后台静默启停脚本与通用配置文件。
最近突然想在手机上也能看到电脑上的文件,找了半天都说需要一个公网ip,对于鄙人白嫖党肯定是不太接受的。最后也是成功找到了一个赛博活佛Cloudflare提供的工具。
感谢 xiaoyuboi/cloudflare-tunnel-skill↗ 提供方法方向,可以直接使用这个 skill 导入 Claude 帮你完成。下文仅当记录了。
一、整体架构
流量链路
外部用户 → https://你的域名 → Cloudflare 边缘节点(HTTPS 终止,CDN 缓存) → Cloudflared 加密隧道(QUIC / HTTP2) → 本机 127.0.0.1:端口(任意本地 HTTP 服务)核心思路:本地跑一个 HTTP 服务(文件浏览、API、网站都可以),通过 Cloudflared 隧道暴露到公网,Cloudflare 负责 HTTPS、CDN 和 DDoS 防护。
本文示例
为方便演示,使用 Python 写了一个支持目录导航和前后切换的文件管理器。读者可以替换为任何 HTTP 服务(Node.js、Nginx、网盘程序等)。
tips:ai时代不会的可以直接找ai帮你完成或问ai帮你辅助完成。
我这里使用了固定域名的方式(命名隧道 cloudflared tunnel run <名称>),与临时域名(quick tunnel trycloudflare.com)不同:命名隧道需先完成第三节的初始化(tunnel create + route dns),之后每次启动都用同一个域名。如果你希望每次启动自动获取临时域名,可以使用同目录下的 tunnel_helper.py quick 子命令。本文的 VBS 脚本是按固定域名写法设计的。对于有关Cloudflare域名解析等,可以参考查看之前创建博客的文章。关于没有类似vercel给的DNS配置可以直接跳过,没有问题。
二、前置环境
- Python 3:用于运行本地服务
- Cloudflared ↗:下载即安装,无界面。可在命令行执行
cloudflared --version验证 - 域名 DNS 已托管到 Cloudflare 并生效
- 推荐:Cloudflare 面板 SSL/TLS →
Full,开启「始终使用 HTTPS」
三、隧道初始化(仅首次)
使用 CMD,依次执行:
:: 登录 Cloudflare 授权(会弹出浏览器)cloudflared tunnel login
:: 创建隧道,名称自定(这里叫 pictures)cloudflared tunnel create pictures
:: 绑定子域名到隧道cloudflared tunnel route dns pictures 你的子域名.你的域名完成后在 %USERPROFILE%\.cloudflared\ 下会生成隧道凭证 JSON,后续启动隧道时会自动使用,无需上传到 Cloudflare 控制台。
四、本地服务
本地需要运行一个 HTTP 服务监听 127.0.0.1,可以是任意程序:
:: 最简单的静态文件服务py -m http.server 3000 --bind 127.0.0.1 --directory F:\share
:: 或者任何本地 HTTP 服务node server.jsnginx -c ...实际使用时推荐自己写一个服务程序,好处是可以添加:
- Range 请求支持(视频拖拽快进)
- ETag / 缓存头(减少重复传输)
- 流式传输(大文件不爆内存)
本文附带的 gallery_server.py 实现了上述优化,同时提供了一个支持目录导航的文件管理器界面。
五、配置文件
将可变参数集中到 config.json,修改端口、目录、域名时无需改动脚本代码:
{ "root_dir": "F:\\share", "port": 3000, "bind": "127.0.0.1", "public_url": "https://你的子域名.你的域名"}Python 服务端通过 --get <key> 子命令暴露配置值,VBS 脚本通过 WshShell.Exec 动态读取:
py gallery_server.py --get port:: 输出: 3000
py gallery_server.py --get running:: 输出: 1(运行中)或 0(未运行)
py gallery_server.py --kill:: 通过 PID 文件精确终止进程服务启动后会在 .cloudflare-tunnel\ 下写入 server.pid,用于状态检测和精确终止,避免仅靠端口查杀的不确定性。
六、启动脚本 StartAll.vbs
这是固定域名(命名隧道)的写法。 脚本通过
cloudflared tunnel run pictures启动预先创建好的命名隧道,使用--config指定项目内的pictures.yml配置文件,域名固定不变。如果需要每次启动自动获取trycloudflare.com临时域名,请使用tunnel_helper.py quick子命令。
保存时注意编码选 ANSI(GBK),否则中文弹窗会乱码。
Option ExplicitDim WshShell, fsoSet WshShell = CreateObject("WScript.Shell")Set fso = CreateObject("Scripting.FileSystemObject")Dim BasePath, port, publicUrl, configPath, cloudflaredExeBasePath = fso.GetParentFolderName(WScript.ScriptFullName)configPath = BasePath & "\.cloudflare-tunnel\pictures.yml"
' 从配置文件读取参数Function GetConfig(key) Dim ex, val Set ex = WshShell.Exec("py """ & BasePath & "\gallery_server.py"" --get " & key) val = ex.StdOut.ReadLine() Set ex = Nothing GetConfig = valEnd Function
port = GetConfig("port")publicUrl = GetConfig("public_url")cloudflaredExe = GetConfig("cloudflared_path")If cloudflaredExe = "" Then cloudflaredExe = "cloudflared"
' Step 1: 查杀残留进程WshShell.Run "cmd /c for /f ""tokens=5"" %a in ('netstat -ano ^| findstr /R ""TCP.*:" & port & "[ ].*LISTENING""') do taskkill /f /pid %a /t >nul 2>&1", 0, TrueWshShell.Run "taskkill /f /im cloudflared.exe /t >nul 2>&1", 0, TrueWScript.Sleep 1200
' Step 2: 启动本地服务(后台静默,无窗口)WshShell.Run "py """ & BasePath & "\gallery_server.py""", 0, False
' Step 3: 等待服务就绪(轮询 PID 文件 + 进程存活检测,最长 15 秒)Dim maxWait, waited, runningmaxWait = 15waited = 0Do While waited < maxWait WScript.Sleep 1000 waited = waited + 1 running = GetConfig("running") If running = "1" Then Exit DoLoop
If running <> "1" Then WshShell.Popup "Gallery server 启动超时(" & maxWait & " 秒)", 10, "启动失败", 16 WScript.Quit 1End If
' Step 4: 启动命名隧道(固定域名,读取项目内的 pictures.yml 配置)WshShell.Run """" & cloudflaredExe & """ --config """ & configPath & """ --no-autoupdate --protocol quic tunnel run pictures", 0, False
' Step 5: 弹窗提示Dim msgTextmsgText = "本地服务 & Cloudflare 隧道已后台运行" & vbCrLf & _ "访问地址:" & publicUrl & vbCrLf & _ "关闭请运行 StopAll.vbs"WshShell.Popup msgText, 8, "启动成功", 64
Set WshShell = NothingSet fso = Nothing启动流程
| 步骤 | 动作 | 说明 |
|---|---|---|
| Step 1 | 查杀残留进程 | 精确匹配端口号边界([ ] 防止 :3000 误杀 :30001),同时清理上次的 Cloudflared |
| Step 2 | 启动 Python 服务 | 后台静默运行,写入 server.pid |
| Step 3 | 等待服务就绪 | 每秒通过 --get running 检测进程存活,超时则弹窗报错退出 |
| Step 4 | 启动命名隧道 | 使用 --config 指定项目内 pictures.yml,--no-autoupdate 禁止自动更新 |
| Step 5 | 弹窗提示 | 显示访问地址,8 秒后自动关闭 |
关键参数说明
| 参数 | 含义 |
|---|---|
TCP.*:PORT[ ] | 正则精确匹配端口边界,[ ] 确保 :3000 不会匹配到 :30001,兼容 IPv4/IPv6 |
tokens=5 | 提取 netstat -ano 输出第 5 列(PID)。netstat 输出列序固定,IPv4/IPv6 格式一致 |
%a | cmd /c 下 FOR 变量必须单百分号,%%a 仅用于 .bat 文件 |
^| | 转义管道符,让外层 cmd 将其传递给内层 |
0, False | 隐藏窗口 + 不等待返回(后台运行) |
/t | taskkill 连带子进程一起关闭 |
--config | 指定隧道配置文件,命名隧道必加,否则 Cloudflared 不会读取项目内的 ingress 规则 |
--no-autoupdate | 禁止 Cloudflared 自动更新,避免后台下载占用带宽 |
--protocol quic | Cloudflared 使用 QUIC 连接边缘节点,比 HTTP2 延迟更低 |
七、停止脚本 StopAll.vbs
Option ExplicitDim WshShell, fsoSet WshShell = CreateObject("WScript.Shell")Set fso = CreateObject("Scripting.FileSystemObject")Dim BasePath, port, execBasePath = fso.GetParentFolderName(WScript.ScriptFullName)
' 从配置文件读取端口Set exec = WshShell.Exec("py """ & BasePath & "\gallery_server.py"" --get port")port = exec.StdOut.ReadLine()Set exec = Nothing
' Step 1: 通过 PID 文件精确终止 gallery_server(优先,不会误杀)WshShell.Run "py """ & BasePath & "\gallery_server.py"" --kill", 0, True
' Step 2: 兜底 —— 按端口查杀残留(防 PID 文件被意外删除)WshShell.Run "cmd /c for /f ""tokens=5"" %a in ('netstat -ano ^| findstr /R ""TCP.*:" & port & "[ ].*LISTENING""') do taskkill /f /pid %a /t >nul 2>&1", 0, True
' Step 3: 关闭所有 Cloudflared 隧道进程WshShell.Run "taskkill /f /im cloudflared.exe /t >nul 2>&1", 0, True
WScript.Sleep 400WshShell.Popup "服务已全部关闭", 3, "停止成功", 64
Set WshShell = NothingSet fso = Nothing停止采用三层保障:
| 优先级 | 方式 | 说明 |
|---|---|---|
| 1 | --kill(PID 文件) | 精确终止,不会误杀其他 Python 进程 |
| 2 | 端口查杀(netstat) | 兜底方案,防止 PID 文件被意外删除后无法停止 |
| 3 | 进程名查杀(taskkill /im) | 关闭所有 Cloudflared .exe |

理论上运行后的文件夹下文件
八、隧道配置文件 pictures.yml
命名隧道需要配置文件指定 ingress 规则(哪个域名 → 哪个本地地址):
tunnel: picturescredentials-file: C:\Users\你的用户名\.cloudflared\<tunnel-id>.json
ingress: - hostname: 你的子域名.你的域名 service: http://127.0.0.1:3000 - service: http_status:404
credentials-file是第三节cloudflared tunnel create生成的凭证文件路径。如果不指定,Cloudflared 会尝试自动查找,但显式指定更可靠。
九、附:文件管理器 gallery_server.py
本文附带了一个 Python 实现的文件管理器,可作为本地服务的起点或直接使用:
功能:
- 目录导航(面包屑路径 + ”..” 返回上级)
- 显示所有文件类型(文件夹、图片、视频、文档等)
- 文件夹排在前面,点击进入子目录
- 媒体文件(图片/视频)点击进入全屏查看器,支持上/下一张切换,右上角 X 关闭
- 非媒体文件点击直接下载,
Content-Disposition头确保浏览器以正确文件名和扩展名保存 - 键盘
←→切换、Esc返回列表 - 移动端滑动切换
- 相邻文件自动预加载(prefetch),切换几乎无等待
运维特性:
- PID 文件:启动时写入
.cloudflare-tunnel\server.pid,关闭时自动清理 --get running:检测进程是否存活(Windows 上通过OpenProcess查询句柄)--kill:通过 PID 文件精确终止进程,StopAll.vbs 优先使用此方式- 端口占用保护:bind 失败时打印明确错误信息并提示
--kill命令
性能优化:
- HTTP Range 请求(206 Partial Content):视频可拖拽快进
- ETag + Last-Modified:浏览器缓存校验,未改动文件返回 304
- 流式传输:64KB 分块读取,不将整个文件加载到内存
- Cache-Control:媒体文件缓存 1 小时
自定义 favicon: 将 favicon.ico 放到脚本同目录下,自动生效,无需改代码。
通用性: gallery_server.py 只是一个示例本地服务。替换为任何监听 127.0.0.1:端口 的 HTTP 程序,VBS 脚本和隧道配置无需改变。
十、常见问题
1. StopAll.vbs 无法关闭进程
症状: 运行 StopAll.vbs 后,Python 进程和 Cloudflared 仍在运行。
根因: 网上流传的 tokens=-1 是无效的 for /f 语法(CMD 不支持负索引取最后一列),会被静默回退为 tokens=1,提取到的是 TCP 而非 PID。同时 %%a 仅在 .bat 文件中有效,cmd /c 命令行模式必须用 %a。
修复: tokens=5 + %a。(上文脚本已使用正确写法)
2. VBS 中文弹窗显示乱码
根因: 文件编码为 UTF-8,但 VBScript 引擎按系统 ANSI 代码页(中文 Windows 为 GBK)读取。
修复: 用记事本另存为 ANSI 编码,或用 Python 转码:
with open('xxx.vbs', 'r', encoding='utf-8') as f: content = f.read()with open('xxx.vbs', 'w', encoding='gbk') as f: f.write(content)3. Cloudflared 报错 “failed to request quick Tunnel”
通常是网络问题或 Cloudflared 版本过旧,升级到最新版即可。
4. 如何更换本地服务程序
修改 StartAll.vbs 中启动本地服务的那一行,将 py gallery_server.py 替换为你的启动命令即可。端口和域名在 config.json 中统一管理。
5. 如何添加其他端口 / 多服务
在 config.json 中增加字段,在 StartAll.vbs 中通过 GetConfig() 读取,复制端口查杀和服务启动的两段代码即可。Cloudflared 支持通过配置文件同时暴露多个服务。
十一、安全说明
--bind 127.0.0.1确保本地服务仅本机可访问,局域网其他设备无法直连- Python
http.server无上传接口,PUT/DELETE 等方法均不可用,只读安全 --directory锁定根目录,无法通过../跨目录读取- Cloudflare 提供免费 DDoS 防护和 SSL 证书
- 如需更细粒度控制(IP 白名单、速率限制),在 Cloudflare 面板 WAF 中配置