Developers

Начало работы с URnetwork SDK

13 мин чтенияView as markdown ↗

Этот гайд ведёт разработчика от пустого проекта до первого подключения: получить JWT аккаунта, установить биндинг для своей платформы, поднять устройство и убедиться, что трафик идёт. SDK — это один Go-модуль, github.com/urnetwork/sdk, открытый через три слоя биндингов: биндинги gomobile для Android и Apple, сборка WebAssembly для JavaScript и c-shared-библиотеки с курируемым C ABI для всего остального. Об архитектуре за каждым биндингом читайте тур по 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): свободно линкуйте его в приложения с закрытым кодом. Пофайловый копилефт обязывает делиться только изменениями собственных файлов SDK.

Вход

Каждый сниппет ниже принимает byJwt: JWT аккаунта, подписанный токен входа, который API URnetwork выдаёт при аутентификации аккаунта. Отдельной системы API-ключей нет.

Ваше приложение один раз проводит процесс входа через API-клиент SDK. authLogin сообщает методы аутентификации идентификатора; authLoginWithPassword завершает вход (authVerify довершает код из почты или SMS), а кошельковая аутентификация и 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 (плюс -sources.jar для навигации в IDE) и положите их в каталог, который ваш 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; среза iOS-симулятора для Intel нет.

JavaScript. Npm-пакет — @urnetwork/sdk-js, загрузчик только для браузера, который подтягивает wasm_exec.js из Go плюс wasm SDK и инстанцирует их на странице. Ставьте тег nightly; он нарезается из текущего SDK по той же датированной схеме версий и несёт wasm. latest отстаёт на месяцы и wasm не несёт вовсе, поэтому простой npm install оставляет вам загрузчик, которому нечего загружать.

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

Wasm — примерно 43 МБ, целиком ядро Go, так что он не уменьшится, но хорошо жмётся: раздавайте его как статический ассет (gzip или brotli, с жёстким кешем) и загружайте лениво, только когда пользователь добирается до поверхности подключения. Никогда не позволяйте бандлеру инлайнить или преобразовывать его и всегда поставляйте sdk.wasm и wasm_exec.js из одной сборки; клей ABI-спарен с тулчейном Go, который скомпилировал wasm, и сборка 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-обёртка над ним, требующая nlohmann/json в вашем include-пути.

Всё, у чего есть C-интерфейс внешних функций (Rust bindgen, Python ctypes, C# P/Invoke), ложится на этот ABI из хендлов и JSON; заголовок C++ — единственное языковое удобство. С MSVC сначала сгенерируйте библиотеку импорта из файла .def:

lib /def:urnetwork_sdk.def /machine:x64 /out:URnetworkSdk.lib

Из исходников. Нужен Go 1.26+, а для мобильных сначала make init: он закрепляет тот самый gomobile, которым нарезаются биндинги, и устанавливает checksec, который запускает Android-таргет. build_android требует ещё и выставленного ANDROID_NDK_HOME, поскольку срезает .comment через llvm-objcopy из NDK. 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 VM

build_android гейтится на списке пропусков gomobile, поэтому отсутствующий символ — это несовпадение версий, а не молчаливо выпавший биндинг; gomobile не биндит ни срезы структур (списки пересекают границу как SdkStringList, SdkIdList, ...), ни context.Context.

Сторонние оценки безопасности — и их рамки — разобраны в модели угроз.

Разрешение на туннель

Согласие операционной системы — и сам туннель — принадлежат вашему приложению. SDK начинается на пакетном уровне:

  • На Android VpnService — ваш: ваше приложение объявляет его, получает согласие на VPN через VpnService.prepare() и вызывает 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, нацеленный на приватное хранилище приложения (каталог держит локальное состояние, включая учётные данные), создайте сетевое пространство, затем установите JWT аккаунта на его API-клиенте. Ключ — имя хоста плюс имя окружения; продакшен — ("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 и качает пакеты в обе стороны, пока вы его не закроете. Не трогайте fd из Java снова после отсоединения.

Три аргумента конструктора заслуживают внимания. instanceId: сгенерируйте один Sdk.newId() при первом запуске, сохраните и переиспользуйте всю жизнь установки (одно живое устройство на процесс); свежий id на каждый запуск добавляет фантомную запись в список устройств аккаунта. keyMaterial: null годится для чистого клиента, но если устройство когда-нибудь будет раздавать ёмкость, сохраните getKeyMaterial() в защищённом хранилище платформы. Это провайдерская идентичность устройства, и её потеря обнуляет историю надёжности провайдера. memoryTargetByteCount: байтовый бюджет, под который устройство подгоняет свои буферы и темп GC; выберите тот, что укладывается в реальный потолок вашего процесса (см. тур). DeviceLocal живёт внутри процесса: умирает ваш процесс — умирает и туннель, поэтому запускайте VpnService как foreground-сервис и пересоздавайте устройство после рестарта с теми же instanceId и ключевым материалом.

iOS / macOS

NEPacketTunnelProvider владеет SdkDeviceLocal и потоком пакетов, а процесс приложения присоединяется к нему как удалённое устройство через loopback:

// 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; расширение читает оттуда 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, — гоняются за одними и теми же глобалами. Инициализируйте один раз в синглтоне уровня модуля, никогда — внутри жизненного цикла компонента.

Две модели устройства:

  • sdk.createProxyDevice(...) — лёгкий клиент размещённых прокси-URL, для случаев, когда всё, что вам нужно, — «прокси-URL с выходом через URnetwork».
  • sdk.createPlatformDeviceRemote({...}) — полный DeviceRemote, говорящий по device-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; идентификаторы — строки UUID, времена — миллисекунды эпохи Unix (0 = нет).
  • Колбэки срабатывают на произвольных потоках под управлением Go; переносите на свой поток сами. Их строки и буферы живут только на время вызова, а хендлы, которые они вам вручают, освобождать вам.
  • Способные на отказ вызовы принимают char** out_error; при сбое туда пишется сообщение, которое вы освобождаете через urnet_free_string. Передайте NULL, чтобы игнорировать текст.

Заголовок несёт одно платформенное разделение: urnet_new_io_loop — насос fd, которым пользуется приложение для Linux, — сидит внутри #if !defined(_WIN32) и отсутствует в Windows-.def. На Windows пакеты двигают urnet_device_local_send_packet и urnet_device_local_add_receive_packet.

Рабочий сквозной пример живёт в cgo/smoke; make smoke_hpp в sdk/cgo собирает смоук-тест C++-обёртки против сборки для хоста и запускает его.

Убедитесь, что работает

Поднимите устройство и проверьте выход: любая проверка «какой у меня IP» через туннель должна теперь сообщать адрес провайдера, а не машины. То же состояние видно в коде и в аккаунте: слушатели срабатывают по мере смены подключения (JavaScript-сниппет выше логирует каждую смену локации по мере прибытия), а устройство появляется в списке устройств аккаунта под deviceDescription, который вы передали. На C ABI urnet_live_handle_count() сообщает живые хендлы; в тестах на утечки проверяйте, что он возвращается к базовой отметке.

Если что-то не так

Поддержка — это открытые каналы: issues в репозиториях (github.com/urnetwork), отзывы о продукте на feedback.ur.io и отчёты о безопасности на [email protected] (политика раскрытия на ur.io/vdp). Платного уровня поддержки SDK сегодня нет. У двух замираний простые объяснения: замирание передачи на работающем туннеле — обычно аккаунт без данных, а JavaScript-загрузчик, которому нечего загружать, — npm-тег latest. Тур покрывает режимы отказа, которые стоит знать до того, как вы отгрузитесь.

Что дальше

  • Пройдите тур по SDK. Архитектура за каждым биндингом: разделение процессов, потоки, аутентификация, переподключение и подбор целевого объёма памяти.
  • Расскажите своим пользователям, к чему они присоединяются. Если ваше приложение ведёт трафик через URnetwork, обзор — каноническое описание пути и того, что может видеть каждая сторона, а модель угроз — полная запись за ним.
  • Посмотрите готовые приложения — опыт, который получат ваши пользователи: Android, iOS, macOS, Windows, Linux и браузер.