Developers

URnetwork SDK 入门

13 分钟阅读View as markdown ↗

这份指南带开发者从空项目走到第一次连接:拿到账户 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_addresswallet_messagewallet_signature,永远不发密钥,也没有任何 URnetwork 界面会索要钱包的私钥或助记词。不带任何认证方式的 networkCreate 就是即时账户路径:它铸出一个永久账户并返回一个 seedphrase — URnetwork 为该账户准备的自有恢复短语,由服务器生成、只交还一次。当场把它展示给用户,否则这个账户就没有任何找回路径。

每条路径都返回 by_jwt。持久化它,调用 setByJwt,并把它传给设备构造函数。你不用写任何刷新代码;Api 的令牌管理器负责轮换 JWT。把硬性认证失败当作"重新跑一遍登录"。

账户同时是计费单位:设备持有谁的 JWT,计量的就是谁的套餐。免费是每日流量额度,Pro 是大额月度额度;当前数字见 ur.io/products。设计上应当一个终端用户一个账户 — 即时账户路径让这件事毫无摩擦 — 而不是用一个内嵌账户把所有用户的用量和行为汇集成单一主体。账户流量用尽时,传输会停摆直到额度刷新,所以在你的 UX 里要说"流量用完了",而不是抛一个笼统的网络错误。

SDK 还掌管付费套餐流程,因为客户端绝不能自报价格:用 createSolanaPaymentIntent 登记一笔意向(reference 来自 createPaymentReferenceplan"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 前缀(SdkDeviceLocalSdkNetworkSpace 等)。xcframework 携带 ios/arm64iossimulator/arm64macos/arm64macos/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 to

wasm 约 43 MB,是整个 Go 核心,所以它不会变小,但它压缩得很好:把它作为静态资产伺服(gzip 或 brotli,强缓存),并且懒加载 — 只在用户到达连接界面时才加载。绝不要让打包器内联或改写它,并且永远从同一次构建里同时发布 sdk.wasmwasm_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 剥离 .commentsdk/build-android.shsdk/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 VM

build_android 以 gomobile 的跳过清单为闸门,所以符号缺失是版本不匹配,而不是绑定被悄悄丢掉;gomobile 不绑定结构体切片(列表以 SdkStringListSdkIdList 等形式过境),也不绑定 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.comapi.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_pemrpc_client_pemrpc_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_packeturnet_device_local_add_receive_packet 搬运数据包。

一个能跑通的端到端示例在 cgo/smoke;在 sdk/cgomake 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 标签。发布之前要了解的失败模式,见导览

接下来做什么

  • 走一遍 SDK 导览 每个绑定背后的架构:进程拆分、线程、认证、重连,以及内存目标的设定。
  • 告诉你的用户他们在加入什么。 如果你的应用把流量经 URnetwork 路由,概览是关于路径和各方所见的权威叙述,威胁模型是它背后的完整记录。
  • 看看成品应用,了解你的用户将得到的体验: AndroidiOSmacOSWindowsLinux,以及 浏览器