URnetwork SDK 入门
这份指南带开发者从空项目走到第一次连接:拿到账户 JWT,安装你所在平台的绑定,拉起一个设备,确认流量在跑。SDK 是一个 Go 模块,github.com/urnetwork/sdk,通过三个绑定层暴露:面向 Android 和 Apple 的 gomobile 绑定、面向 JavaScript 的 WebAssembly 构建,以及带一套精选 C ABI 的 c-shared 库供其余一切使用。每个绑定背后的架构见 SDK 导览。
URnetwork 使用成员运行的出口设备。在中继路径上,提供者收不到你用户们的来源 IP。基于 SDK 构建的设备也默认把发往提供者的流量加密。完整模型见 URnetwork 的工作原理。
你需要什么
三个层面向的都是官方 URnetwork 应用赖以构建的同一个核心:设备(连接引擎)、API 客户端、网络空间配置模型(你对话的是哪个平台部署,用什么端点和标志),以及视图控制器 — 为常见屏幕准备的无界面"状态加事件"对象,你可以把 UI 绑上去,也可以无视它们。背后的运营方 API 文档在 /docs/api。你是在 URnetwork 的托管平台上构建自己的应用:API、中继和提供者就是线上服务,你的用户自带或创建账户。
各绑定的平台下限:
- Android — API 级别 24 或更新。Java 包名是
com.bringyour.sdk。 - iOS / macOS — iOS 16.0 和 macOS 13.5。
- JavaScript — 仅浏览器。
- cgo — Ubuntu 22.04+(glibc 2.35+)或 Windows 10+,amd64 和 arm64。
- 从源码构建 — Go 1.26+。
SDK 采用 MPL-2.0(Mozilla Public License):可以自由链接进闭源应用。文件级 copyleft 只要求分享对 SDK 自身文件的修改。
登录
下面每段代码都需要一个 byJwt:账户 JWT,账户完成认证时由 URnetwork API 签发的签名登录令牌。没有单独的 API key 体系。
你的应用通过 SDK 的 API 客户端跑一次登录流程。authLogin 报告某个标识符可用的认证方式;authLoginWithPassword 完成登录(authVerify 收尾邮件或短信验证码),钱包认证和 networkCreate 是并行的入口。钱包认证只凭签名:你发送 wallet_address、wallet_message 和 wallet_signature,永远不发密钥,也没有任何 URnetwork 界面会索要钱包的私钥或助记词。不带任何认证方式的 networkCreate 就是即时账户路径:它铸出一个永久账户并返回一个 seedphrase — URnetwork 为该账户准备的自有恢复短语,由服务器生成、只交还一次。当场把它展示给用户,否则这个账户就没有任何找回路径。
每条路径都返回 by_jwt。持久化它,调用 setByJwt,并把它传给设备构造函数。你不用写任何刷新代码;Api 的令牌管理器负责轮换 JWT。把硬性认证失败当作"重新跑一遍登录"。
账户同时是计费单位:设备持有谁的 JWT,计量的就是谁的套餐。免费是每日流量额度,Pro 是大额月度额度;当前数字见 ur.io/products。设计上应当一个终端用户一个账户 — 即时账户路径让这件事毫无摩擦 — 而不是用一个内嵌账户把所有用户的用量和行为汇集成单一主体。账户流量用尽时,传输会停摆直到额度刷新,所以在你的 UX 里要说"流量用完了",而不是抛一个笼统的网络错误。
SDK 还掌管付费套餐流程,因为客户端绝不能自报价格:用 createSolanaPaymentIntent 登记一笔意向(reference 来自 createPaymentReference,plan 取 "monthly" 或 "yearly"),从结果里取 amountUsd,传给 buildSolanaPaymentUrl。C ABI 有意向调用,还没有 URL 构建器。
安装
预构建产物是 github.com/urnetwork/build 上的发布资产。版本号基于日期,vYYYY.M.D-(例如 v2026.7.22-999364023),不是 SemVer:钉住一个发布版,升级要有意为之,因为版本号说的是它何时切出,而不是 API 有没有变。
Android。 下载 URnetworkSdk-.aar(加上供 IDE 导航用的 -sources.jar),丢进你的 Gradle 模块已经扫描的目录,例如:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}iOS / macOS。 下载 URnetworkSdk.xcframework.zip,解压,在本地 Swift 包里作为 binaryTarget 引用:
// Package.swift
targets: [
.binaryTarget(name: "URnetworkSdk", path: "URnetworkSdk.xcframework")
]所有类型都带 Sdk 前缀(SdkDeviceLocal、SdkNetworkSpace 等)。xcframework 携带 ios/arm64、iossimulator/arm64、macos/arm64 和 macos/amd64;没有 Intel 的 iOS 模拟器切片。
JavaScript。 npm 包是 @urnetwork/sdk-js,一个仅限浏览器的加载器,它抓取 Go 的 wasm_exec.js 和 SDK 的 wasm,并在页面里实例化。安装 nightly 标签;它按同一套日期版本方案从当前 SDK 切出,并附带 wasm。latest 落后数月,而且完全不带 wasm,所以一句普通的 npm install 只会给你一个无物可载的加载器。
npm install @urnetwork/sdk-js@nightly # then pin the version it resolved towasm 约 43 MB,是整个 Go 核心,所以它不会变小,但它压缩得很好:把它作为静态资产伺服(gzip 或 brotli,强缓存),并且懒加载 — 只在用户到达连接界面时才加载。绝不要让打包器内联或改写它,并且永远从同一次构建里同时发布 sdk.wasm 和 wasm_exec.js;胶水层与编译该 wasm 的 Go 工具链按 ABI 配对,SDK 的构建以两者逐字节一致为闸门。
cgo。 c-shared 库,与上架的 URnetwork Linux 和 Windows 应用所构建于的是同一层,旁边带两个精选头文件:
libURnetworkSdk.so— Linux。URnetworkSdk.dll(+urnetwork_sdk.def)— Windows。urnetwork_sdk.h— 纯 C ABI。urnetwork_sdk.hpp— 其上的 header-only C++17 RAII 封装,要求 include 路径上有 nlohmann/json。
任何带 C 外部函数接口的语言(Rust bindgen、Python ctypes、C# P/Invoke)都能对上这套"句柄加 JSON"的 ABI;C++ 头文件是唯一的语言便利。用 MSVC 时,先从 .def 文件生成导入库:
lib /def:urnetwork_sdk.def /machine:x64 /out:URnetworkSdk.lib从源码。 你需要 Go 1.26+,移动端还要先 make init:它钉住切绑定所用的那个 gomobile,并安装 Android 目标要跑的 checksec。build_android 还需要设置 ANDROID_NDK_HOME,因为它用 NDK 的 llvm-objcopy 剥离 .comment。sdk/build-android.sh 和 sdk/build-ios.sh 把这两步都包好了。在 sdk/build 下:
make init # pinned gomobile + checksec; run before the mobile targets
make build_android # AAR
make build_apple # xcframework (build_ios is an alias)
make build_js # wasm + loader
make build_linux # c-shared .so, cross-compiled with zig
make build_windows # c-shared .dll; not in `all` — the shipped one builds in a VMbuild_android 以 gomobile 的跳过清单为闸门,所以符号缺失是版本不匹配,而不是绑定被悄悄丢掉;gomobile 不绑定结构体切片(列表以 SdkStringList、SdkIdList 等形式过境),也不绑定 context.Context。
第三方安全评估及其局限在威胁模型里有说明。
隧道权限
操作系统的同意步骤和隧道本身都属于你的应用。SDK 从数据包层开始:
- 在 Android 上,
VpnService是你的:你的应用声明它,用VpnService.prepare()获得 VPN 同意,再调用establish()。SDK 从那个文件描述符处接手。 - 在 Apple 平台上,这种拆分是 Apple 的要求,不是 SDK 的:数据包隧道运行在一个内存限制很紧的独立 NetworkExtension 里,所以隧道提供者进程拥有设备,你的应用进程远程挂接上去。
- 在浏览器里,页面不可能拥有网络接口,所以没有权限可问,JavaScript 层里也没有任何东西为页面自身的流量开隧道。两种 JavaScript 设备模型都改为驱动平台上的一个设备。URnetwork 扩展覆盖浏览器标签页;原生应用覆盖整台机器。
关于 URnetwork 对连接记录什么,见威胁模型。
连接
新设备从网络默认值启动。客户端-提供者会话开箱即密封:setPerformanceProfile 出厂即带 PostQuantumEncryption 开启 — 它就是应用里"后量子加密"(Post Quantum Encryption)控制项背后的标志 — 并且当前所有提供者构建都启用应答一侧。开启该标志时客户端是故障即关闭(fail-closed)的:它不会明文承载应用数据,遇到密封不了的提供者会跳过,而不是不密封就用。把它关掉,流量才又能走标准路径。AllowDirect 默认关闭;它是自行选择的提速设置,会去掉匿名化那一跳、把你用户的真实 IP 交给那个提供者,应用把它反相呈现为"强匿名化"(Strong Anonymization),托管设备配置一律强制它关闭。防泄漏由你自己接线:断网保护(kill switch)的原语是 setRouteLocal,设备启动时允许本地路由,setRouteLocal(false) 让隧道断开时流量停住而不是回落。SDK 不内嵌任何分析或崩溃上报;它唯一连接的是平台端点,以及设备使用的中继和提供者。
Android
引导顺序:创建一个指向应用私有存储的 NetworkSpaceManager(该目录保存本地状态,包括凭证),创建网络空间,然后在它的 API 客户端上设置账户 JWT。键是主机名加环境名;生产环境是 ("ur.network", "main"),创建空间的是 updateNetworkSpace,而 getNetworkSpace 只读取已存在的空间,所以全新安装时它返回 null。URL 由键推导(https://api.、wss://connect.;非 main 环境给服务加前缀),但推导优先采用 migrationHostName,而生产环境把它设为 bringyour.com:上架应用连的是 api.bringyour.com,api.ur.network 并不解析。然后创建设备,把隧道的文件描述符交给它:
import com.bringyour.sdk.Sdk
val manager = Sdk.newNetworkSpaceManager(context.filesDir.absolutePath)
val key = Sdk.newNetworkSpaceKey("ur.network", "main")
val networkSpace = manager.updateNetworkSpace(key) { it.migrationHostName = "bringyour.com" }
networkSpace.api.setByJwt(byJwt)
val device = Sdk.newDeviceLocalWithMemoryTarget(
networkSpace,
byJwt,
deviceDescription, // free-form, shown in the account's device list
deviceSpec, // e.g. Build.MODEL
appVersion,
Sdk.newId(), // instanceId; persist and reuse per install
/* enableRpc */ false,
keyMaterial, // persisted DeviceLocalKeyMaterial, or null for ephemeral
memoryTargetByteCount,
)
// inside your VpnService, after establish():
val detachedFd = pfd.detachFd() // ParcelFileDescriptor -> raw fd
val ioLoop = Sdk.newIoLoop(device, detachedFd) { /* done callback */ }newIoLoop 取得已分离 fd 的所有权,并双向泵送数据包直到你关闭它。分离之后不要再从 Java 一侧碰那个 fd。
三个构造函数参数值得上心。instanceId:首次运行生成一个 Sdk.newId(),持久化它,并在这次安装的整个生命周期里复用(每进程一个存活设备);每次启动都换新 id 会往账户的设备列表里塞一个幽灵条目。keyMaterial:纯客户端传 null 就行,但如果这个设备将来会提供容量,就把 getKeyMaterial() 持久化在平台安全存储里。它是设备的提供者身份,弄丢它会清零该提供者的可靠性历史。memoryTargetByteCount:设备据以确定缓冲区大小和 GC 节奏的字节预算;挑一个符合你进程真实上限的值(见导览)。DeviceLocal 是进程内的:进程死了,隧道跟着死,所以把 VpnService 跑成前台服务,并在重启时用同一个 instanceId 和密钥材料重建设备。
iOS / macOS
NEPacketTunnelProvider 拥有 SdkDeviceLocal 和数据包流,应用进程经环回作为远程设备挂接上去:
// in the packet tunnel provider (owns the device):
var err: NSError?
let device = SdkNewDeviceLocalWithMemoryTarget(
networkSpace, byJwt, deviceDescription, deviceSpec, appVersion,
instanceId, /* enableRpc */ true, keyMaterial, memoryTarget, &err)
// in the app process (attaches to it):
let remote = SdkNewDeviceRemoteWithDefaults(networkSpace, byJwt, instanceId, &err)
try remote?.setRpcServer(clientPem, serverCertPem: serverCertPem, hostPort: hostPort)应用铸出密钥材料和 RPC PEM(SdkGenerateDeviceRpcKeyMaterial),经 NETunnelProviderProtocol 的 provider configuration 交给扩展;扩展从那里读取 rpc_server_pem、rpc_client_pem 和 rpc_listen_hostport。这条路径上没有 App Group。拆分为何存在、重连如何工作,见导览。
JavaScript
import { URNetwork } from "@urnetwork/sdk-js";
const sdk = await URNetwork.init({
wasmUrl: "/wasm/sdk.wasm",
wasmExecUrl: "/wasm/wasm_exec.js",
});wasm 把它的导出注册在 window 上,所以每页只跑一个模块实例。init 在模块内是幂等的(第二次调用返回同一个实例),但两份加载器 — 比如共享同一 realm 的两个 bundle 或两个 frame — 会争抢同一批全局量。在模块级单例里初始化一次,绝不要放进组件生命周期。
两种设备模型:
sdk.createProxyDevice(...)— 托管代理 URL 的轻量客户端,适合你只需要"一个经 URnetwork 出网的代理 URL"的场景。sdk.createPlatformDeviceRemote({...})— 一个完整的DeviceRemote,经 websocket 对一台托管设备说设备 RPC,适合真正的连接 UI(位置、监听器、统计):
const device = sdk.createPlatformDeviceRemote({
apiUrl: "api.bringyour.com",
platformUrl: "connect.bringyour.com",
byJwt,
proxyUrl, // from the platform's proxy config endpoint
signedProxyId, // HMAC auth token, not the JWT — see the tour
});
const unsub = device.addConnectLocationChangeListener((loc) => {
console.log("location:", loc?.name);
});
device.setConnectLocation({ bestAvailable: true });createPlatformDeviceRemote 在加载的 wasm 早于 DeviceRemote 绑定时会抛出异常;那是 npm 标签太旧的症状。
cgo
ABI 契约,简述:
- 对象是不透明的
uint64_t句柄。urnet_release(h)释放句柄但不停止对象,所以凡是有*_close/*_stop的,先调它。 - 返回的
char*字符串归调用方所有;用urnet_free_string释放。 - 结构化数据以 UTF-8 JSON 字符串跨越边界;id 是 UUID 字符串,时间是 Unix 纪元毫秒(
0= 无)。 - 回调在任意 Go 管理的线程上触发;请自行封送到你自己的线程。它们的字符串和缓冲区只在那次调用期间存活,它们交给你的句柄由你负责释放。
- 可失败的调用带一个
char** out_error;失败时它被置为一条消息,用urnet_free_string释放。传NULL可忽略文本。
头文件带一处平台分叉:urnet_new_io_loop — Linux 应用用的 fd 泵 — 位于 #if !defined(_WIN32) 内,且不在 Windows 的 .def 里。在 Windows 上用 urnet_device_local_send_packet 和 urnet_device_local_add_receive_packet 搬运数据包。
一个能跑通的端到端示例在 cgo/smoke;在 sdk/cgo 里 make smoke_hpp 会针对宿主构建编译 C++ 封装的冒烟测试并运行它。
确认生效
把设备拉起来,检查出口:任何走隧道的"我的 IP 是什么"检查现在都应报告某个提供者的地址,而不是这台机器的。同样的状态在代码里和账户里都看得到:连接变化时监听器会触发(上面的 JavaScript 代码片段逐条打出每次位置变化),设备也会以你传入的 deviceDescription 出现在账户的设备列表里。在 C ABI 上,urnet_live_handle_count() 报告存活句柄数;在泄漏测试里断言它回到基线。
如果出了问题
支持就是那些公开渠道:仓库上的 issue(github.com/urnetwork)、产品反馈 feedback.ur.io,以及发往 [email protected] 的安全报告(披露政策见 ur.io/vdp)。今天没有付费的 SDK 支持档。两种停摆有现成的解释:隧道正常但传输停摆,通常是账户流量用尽;JavaScript 加载器无物可载,则是 npm 的 latest 标签。发布之前要了解的失败模式,见导览。