# URnetwork SDK 入门

这份指南带开发者从空项目走到第一次连接：拿到账户 JWT，安装你所在平台的绑定，拉起一个设备，确认流量在跑。SDK 是一个 Go 模块，`github.com/urnetwork/sdk`，通过三个绑定层暴露：面向 Android 和 Apple 的 gomobile 绑定、面向 JavaScript 的 WebAssembly 构建，以及带一套精选 C ABI 的 c-shared 库供其余一切使用。每个绑定背后的架构见 [SDK 导览](/docs/tour-sdk)。

URnetwork 使用成员运行的出口设备。在中继路径上，提供者收不到你用户们的来源 IP。基于 SDK 构建的设备也默认把发往提供者的流量加密。完整模型见 [URnetwork 的工作原理](/docs/overview)。

## 你需要什么

三个层面向的都是官方 URnetwork 应用赖以构建的同一个核心：设备（连接引擎）、API 客户端、网络空间配置模型（你对话的是哪个平台部署，用什么端点和标志），以及视图控制器 — 为常见屏幕准备的无界面"状态加事件"对象，你可以把 UI 绑上去，也可以无视它们。背后的运营方 API 文档在 [/docs/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](https://ur.io/products)。设计上应当一个终端用户一个账户 — 即时账户路径让这件事毫无摩擦 — 而不是用一个内嵌账户把所有用户的用量和行为汇集成单一主体。账户流量用尽时，传输会停摆直到额度刷新，所以在你的 UX 里要说"流量用完了"，而不是抛一个笼统的网络错误。

SDK 还掌管付费套餐流程，因为客户端绝不能自报价格：用 `createSolanaPaymentIntent` 登记一笔意向（`reference` 来自 `createPaymentReference`，`plan` 取 `"monthly"` 或 `"yearly"`），从结果里取 `amountUsd`，传给 `buildSolanaPaymentUrl`。C ABI 有意向调用，还没有 URL 构建器。

## 安装

预构建产物是 [github.com/urnetwork/build](https://github.com/urnetwork/build/releases) 上的发布资产。版本号基于日期，`vYYYY.M.D-<code>`（例如 `v2026.7.22-999364023`），不是 SemVer：钉住一个发布版，升级要有意为之，因为版本号说的是它何时切出，而不是 API 有没有变。

**Android。** 下载 `URnetworkSdk-<version>.aar`（加上供 IDE 导航用的 `-sources.jar`），丢进你的 Gradle 模块已经扫描的目录，例如：

```groovy
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
}
```

**iOS / macOS。** 下载 `URnetworkSdk.xcframework.zip`，解压，在本地 Swift 包里作为 `binaryTarget` 引用：

```swift
// 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`](https://www.npmjs.com/package/@urnetwork/sdk-js)，一个仅限浏览器的加载器，它抓取 Go 的 `wasm_exec.js` 和 SDK 的 wasm，并在页面里实例化。安装 `nightly` 标签；它按同一套日期版本方案从当前 SDK 切出，并附带 wasm。`latest` 落后数月，而且完全不带 wasm，所以一句普通的 `npm install` 只会给你一个无物可载的加载器。

```sh
npm install @urnetwork/sdk-js@nightly   # then pin the version it resolved to
```

wasm 约 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](https://github.com/nlohmann/json)。

任何带 C 外部函数接口的语言（Rust bindgen、Python ctypes、C# P/Invoke）都能对上这套"句柄加 JSON"的 ABI；C++ 头文件是唯一的语言便利。用 MSVC 时，先从 `.def` 文件生成导入库：

```bat
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` 下：

```sh
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 不绑定结构体切片（列表以 `SdkStringList`、`SdkIdList` 等形式过境），也不绑定 `context.Context`。

第三方安全评估及其局限在[威胁模型](/docs/threat-model)里有说明。

## 隧道权限

操作系统的同意步骤和隧道本身都属于你的应用。SDK 从数据包层开始：

- 在 **Android** 上，`VpnService` 是你的：你的应用声明它，用 `VpnService.prepare()` 获得 VPN 同意，再调用 `establish()`。SDK 从那个文件描述符处接手。
- 在 **Apple 平台**上，这种拆分是 Apple 的要求，不是 SDK 的：数据包隧道运行在一个内存限制很紧的独立 NetworkExtension 里，所以隧道提供者进程拥有设备，你的应用进程远程挂接上去。
- 在**浏览器**里，页面不可能拥有网络接口，所以没有权限可问，JavaScript 层里也没有任何东西为页面自身的流量开隧道。两种 JavaScript 设备模型都改为驱动平台上的一个设备。URnetwork 扩展覆盖浏览器标签页；原生应用覆盖整台机器。

关于 URnetwork 对连接记录什么，见[威胁模型](/docs/threat-model)。

## 连接

新设备从网络默认值启动。客户端-提供者会话开箱即密封：`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.<host>`、`wss://connect.<host>`；非 `main` 环境给服务加前缀），但推导优先采用 `migrationHostName`，而生产环境把它设为 `bringyour.com`：上架应用连的是 `api.bringyour.com`，`api.ur.network` 并不解析。然后创建设备，把隧道的文件描述符交给它：

```kotlin
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 节奏的字节预算；挑一个符合你进程真实上限的值（见[导览](/docs/tour-sdk)）。`DeviceLocal` 是进程内的：进程死了，隧道跟着死，所以把 `VpnService` 跑成前台服务，并在重启时用同一个 `instanceId` 和密钥材料重建设备。

### iOS / macOS

`NEPacketTunnelProvider` 拥有 `SdkDeviceLocal` 和数据包流，应用进程经环回作为远程设备挂接上去：

```swift
// 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。拆分为何存在、重连如何工作，见[导览](/docs/tour-sdk)。

### JavaScript

```js
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（位置、监听器、统计）：

```js
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](https://github.com/urnetwork)）、产品反馈 [feedback.ur.io](https://feedback.ur.io)，以及发往 security@ur.io 的安全报告（披露政策见 [ur.io/vdp](https://ur.io/vdp)）。今天没有付费的 SDK 支持档。两种停摆有现成的解释：隧道正常但传输停摆，通常是账户流量用尽；JavaScript 加载器无物可载，则是 npm 的 `latest` 标签。发布之前要了解的失败模式，见[导览](/docs/tour-sdk)。

## 接下来做什么

- **走一遍 [SDK 导览](/docs/tour-sdk)。** 每个绑定背后的架构：进程拆分、线程、认证、重连，以及内存目标的设定。
- **告诉你的用户他们在加入什么。** 如果你的应用把流量经 URnetwork 路由，[概览](/docs/overview)是关于路径和各方所见的权威叙述，[威胁模型](/docs/threat-model)是它背后的完整记录。
- **看看成品应用**，了解你的用户将得到的体验：
  [Android](/docs/getting-started-android)、
  [iOS](/docs/getting-started-ios)、[macOS](/docs/getting-started-macos)、
  [Windows](/docs/getting-started-windows)、
  [Linux](/docs/getting-started-linux)，以及
  [浏览器](/docs/getting-started-browser)。
