返回技术专栏

把建隧道写进代码:ZeroNews 本地 Client API 实战

ZeroNews 本地 Client API 把 "建隧道" 写进代码,解决开发联调中的三大痛点 —— 频繁切换浏览器控制台、隧道生命周期与代码不同步(端口变了公网地址 502)、联调完忘删映射留下安全隐患。Client 启动后会在本机 `127.0.0.1:37271` 暴露 REST API(`/api/v1/*`),应用启动时调用 `POST /api/v1/tunnels` 即可自动分配公网地址并拿到 `public_url`,退出时调 `shutdown` 自动清理;文章给出了 Python、Go、Node.js 三种语言的集成示例,几秒就能完成原本登录控制台要 2-3 分钟的操作。API 刻意只绑定本机、建议 Client 常驻,适合 Webhook 联调、临时预览、CI 集成测试等高频短周期场景。

一、为什么需要内网穿透的自动化隧道?

在我们本地开发应用的过程中,由于涉及不同业务模块之间的 webhook 对接及联调/预览,经常需要用到内网穿透工具将本地应用映射到公网,给其他系统调用或不同位置的同事预览/测试,那这时候往往需要我们登录到内网穿透的平台去添加域名/添加隧道。

开发里常见的几个痛点

  • 上下文切换太频繁。 写代码用的是 IDE,建隧道得跳浏览器。来回切窗口。
  • 隧道生命周期跟代码不同步。 本地服务 Ctrl+C 就停了,控制台里的映射还在。下次启动端口变了(8080 换成 3000),公网地址打回来 502,还得再进控制台改一遍。
  • 容易留安全隐患。 联调完忘了删映射、忘了关隧道,开发用的入口长期暴露在公网上,留下安全隐患。

那有没有一种内网穿透能做到集成在代码里面,能随应用启动而启动,随应用停止而停止,既省创建隧道的操作,同时解决安全隐患?

ZeroNews 内网穿透能很好的解决这个问题。后面会说明 ZeroNews 是什么、本地 Client API 又能帮你干什么。

二、ZeroNews 和本地 Client API

ZeroNews(零讯)是内网穿透工具。机器上跑一个 Agent,在控制台配好映射,外网就能通过 HTTPS 域名访问你本地的服务,比如 127.0.0.1:8080,不需要用户具备公网 IP ,同时域名证书由平台处理。

ZeroNews 不仅支持控制台管理隧道,同时还支持通过 REST API 管理隧道,实现隧道完全自动化。

平时用控制台管理域名、带宽、设备状态,没问题。但开发联调是高频、短周期的操作——更适合用 API 在代码里完成,而不是反复切窗口。

本地 Client API

Client 执行 zeronews start(或 zeronews service start)后,除了连云端跑隧道,还会在本地监听: http://127.0.0.1:37271 这里有 Web UI,也有 REST API(路径前缀 /api/v1/*)。脚本、CI、应用启动逻辑都可以直接调。

常用接口:

  • GET /healthz — 看 API 进程是否正常
  • GET /api/v1/status — Client 有没有连上云端
  • POST /api/v1/tunnels — 建隧道;请求里可以不传 domain,ZeroNews 会分配公网地址,响应里有 public_url
  • GET /api/v1/endpoints — 查隧道是否已落地(status 是否为 active)
  • POST /api/v1/reconnect、POST /api/v1/shutdown — 重连或关闭 Client
  • API 只绑 127.0.0.1,本机才能访问。这是刻意的,别改成 0.0.0.0 对外暴露。

用 API 集成后,几个实际变化:

  • 启动时调接口建隧道,退出时调 shutdown,不用记着去控制台删映射
  • public_url 直接打印在终端,Webhook 地址不用手抄
  • 可以先查 Client 是否在线,再决定要不要建隧道,少碰[地址有了但 502]的情况
  • self-hosted CI 机器上 Client 常驻,Job 里 curl 一下就能临时开入口

三、动手集成

3.1 确认 Client 正常

zeronews start
# 或:zeronews service start
curl -s http://127.0.0.1:37271/healthz
# {"code":200,"data":{"ok":true}}

curl -s http://127.0.0.1:37271/api/v1/status | jq 
'.data.connected'
# true 表示已连上云端

3.2 建隧道(自动分配域名)

请求里只传协议和本地端口,不传 domain:

curl -s -X POST http://127.0.0.1:37271/api/v1/tunnels \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "https",
    "local_ip": "127.0.0.1",
    "local_port": 8080
  }'

返回:

{
  "code": 200,
  "data": {
    "state": "accepted_remote",
    "tunnel_id": "tunnel-abc123",
    "type": "https",
    "local": "127.0.0.1:8080",
    "public_url": "https://x7k2mp.ny.takin.cc"
  }
}

public_url 就是 Webhook 回调地址。accepted_remote 表示控制面已受理;要确认隧道完全通了,可以再查一次 /api/v1/endpoints,看对应项的 status 是不是 active。

后面 Python / Go / Node 示例返回结构相同,不再重复贴 JSON。

Python 中集成

需安装 requests。应用启动时调一次,取出 public_url 即可:

import requests

resp = requests.post("http://127.0.0.1:37271/api/v1/tunnels", json={
    "type": "https",
    "local_ip": "127.0.0.1",
    "local_port": 8080,
})
body = resp.json()
print(body)
print("Webhook 地址:", body["data"]["public_url"])

FastAPI / Django 可以放在启动钩子里;进程退出时如需清理,再 POST /api/v1/shutdown。

Go 中集成

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"type":       "https",
		"local_ip":   "127.0.0.1",
		"local_port": 8080,
	})
	resp, _ := http.Post(
		"http://127.0.0.1:37271/api/v1/tunnels",
		"application/json",
		bytes.NewReader(payload),
	)
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))

	var result struct {
		Data struct {
			PublicURL string `json:"public_url"`
		} `json:"data"`
	}
	json.Unmarshal(body, &result)
	fmt.Println("Webhook 地址:", result.Data.PublicURL)
}

Gin / Echo 在 main() 里、服务 listen 之前调用这段逻辑。

Node.js 中集成

Node 18+ 自带 fetch:

const resp = await fetch('http://127.0.0.1:37271/api/v1/tunnels', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    type: 'https',
    local_ip: '127.0.0.1',
    local_port: 8080,
  }),
})
const body = await resp.json()
console.log(body)
console.log('Webhook 地址:', body.data.public_url)

Express / NestJS 在 listen 之前 await 即可。

补充两点:

要用固定域名,请求体里加上 "domain": "xxx.example.zeronews.cc";

Client 建议 zeronews service start 常驻,别在每次 HTTP 请求里调 shutdown——那会关掉整个 Client,只适合开发退出时用。

在 macOS 上试过(Client v4.x,本地 Node 8080):控制台手工建隧道要登录填表,大概2-3分钟;代码里调 API,几秒就能拿到 public_url,而且不用提前去控制台建域名。

ZeroNews 本地 API 的价值,是把建隧道这件事从[打开控制台点鼠标]变成[应用启动时顺手调个接口]。Webhook 联调、临时预览、CI 里跑集成测试,都适用。