Разработчикам

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

13 мин чтенияОткрыть как Markdown ↗

Установите SDK менеджером пакетов своего языка, затем изучите примеры на разных языках и руководство по сокетам.

Этот гайд ведёт разработчика от пустого проекта до первого подключения: получить JWT аккаунта, установить биндинг для своей платформы, поднять устройство и убедиться, что трафик идёт. SDK — это один Go-модуль, github.com/urnetwork/sdk, открытый через четыре биндинга:

  • Android — ядро, скомпилированное в нативный код и привязанное для Kotlin/Java, поставляется как AAR.
  • iOS / macOS — тот же путь gomobile, привязанный для Swift, поставляется как xcframework.
  • cgo (Windows, Linux) — ядро в виде c-shared-библиотеки за C ABI, с заголовком C++17 поверх него. Любой язык с FFI добирается до него здесь.
  • JavaScript (wasm/веб) — ядро, скомпилированное в WebAssembly и выполняемое на странице под управлением вашего браузерного фреймворка.

Все они выводят наружу одно ядро, поэтому поведение совпадает на всех платформах, — но они не взаимозаменяемы, и различия определяют вашу архитектуру, а не только синтаксис. Прочитайте Какой биндинг, прежде чем выбирать, а о том, как устроен каждый, — тур по SDK.

URnetwork использует выходные устройства, которые держат участники. На ретранслируемых путях провайдеры не получают исходные IP ваших пользователей. Устройства, построенные на SDK, тоже могут шифровать трафик до провайдера, но только после того, как вы включите PostQuantumEncryption; у нового устройства он выключен. Полная модель — в статье Как работает URnetwork.

Что понадобится

Все четыре биндинга выводят наружу одно и то же ядро, на котором построены официальные приложения URnetwork: устройство (движок соединения), API-клиент, конфигурационную модель сетевого пространства (с каким развёртыванием платформы вы говорите, с какими эндпоинтами и флагами) и вью-контроллеры — безголовые объекты «состояние плюс события» для типовых экранов, к которым можно привязать свой UI или игнорировать их. Операторский API за этим задокументирован на /docs/api. Вы строите собственное приложение на размещённой платформе URnetwork: API, ретрансляторы и провайдеры — живой сервис, а аккаунты ваши пользователи приносят или создают.

Минимальные платформы, по биндингам:

  • Android — уровень API 24 и новее. Java-пакет — com.bringyour.sdk.
  • iOS / macOS — iOS 16.0 и macOS 13.5.
  • cgo — Ubuntu 22.04+ (glibc 2.35+) или Windows 10+, amd64 и arm64.
  • JavaScript — только браузер.
  • Сборка из исходников — Go 1.26+.

SDK распространяется под MPL-2.0 (Mozilla Public License): свободно линкуйте его в приложения с закрытым кодом. Пофайловый копилефт обязывает делиться только изменениями собственных файлов SDK.

Какой биндинг

Нативные биндинги могут владеть пакетным путём устройства. Страница браузера не может владеть TUN-интерфейсом операционной системы; её JavaScript-объект Device управляет удалённым устройством. Обновлённый JS SDK также открывает прикладные сокеты через удалённый Device с поддержкой сокетов, включая TCP, UDP, TLS/DTLS и интерфейс JavaScript Direct Sockets. Так эти прикладные соединения маршрутизируются, не превращая страницу браузера в VPN операционной системы.

AndroidiOS / macOScgoJavaScript
Вы пишете наKotlin / JavaSwiftC++ (или любом языке с FFI)JS / TypeScript
Ядро приходит какнативный код, привязка gomobileнативный код, привязка gomobilec-shared-библиотека, C ABIwasm на странице
Выполняет пакетный путьда, в процессе вашего приложенияда, в NetworkExtensionда, в демоне, который пишете вынет
Модель процессоводин процессприложение + расширение, loopback mTLSUI + привилегированная служба, loopback mTLSтолько страница
Артефакт~37 МБ AAR~116 МБ xcframework~21–25 МБ библиотека~43,5 МБ wasm

Во что это вам обходится — в том порядке, в каком это укусит:

  • Android — самый простой: один процесс держит UI и VpnService, поэтому ваше приложение владеет DeviceLocal напрямую, а IoLoop качает tun-fd внутри Go без попакетных JNI-переходов.
  • iOS / macOS — тот же биндинг в более трудной форме. Каждая VPN на Apple выполняет обработку пакетов в отдельном песочничном процессе, поэтому устройство живёт в расширении, а ваше приложение управляет им удалённо. Закладывайте жизненный цикл переприсоединения и лимит памяти расширения с первого дня — это не крайний случай: ОС перезапускает этот процесс в порядке вещей.
  • cgo — самый переносимый и наименее эргономичный. ABI — это хендлы и JSON, а не типизированные объекты, и именно это позволяет дотянуться до него Rust, Python и C#; заголовок C++17 — единственное место, куда вложена эргономика. Привилегированный демон вы тоже пишете сами.
  • JavaScript меняет пакетный путь на охват. Вы получаете поверхность API, вью-контроллеры и DeviceRemote против устройства, размещённого где-то ещё, — и 43 МБ полезной нагрузки для ленивой загрузки.

В вопросе потоков все четыре сходятся на одном правиле: не-view-объекты безопасны для конкурентного доступа, вью-контроллеры — нет. Браузерный JS получает это даром; остальные три — нет. Подробности — в туре.

Вход

Каждый сниппет ниже принимает 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 нет.

cgo (Windows, Linux). 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

JavaScript (браузер и Node). Канонический пакет — @urnetwork/sdk; он содержит загрузчик, объявления TypeScript, wasm_exec.js из Go и соответствующий WASM SDK. Используйте релиз с поддержкой сокетов.

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

WASM содержит сетевое ядро Go, и это большая загрузка. Раздавайте его как статический ассет (gzip или brotli, с жёстким кешем) и загружайте лениво, только когда пользователь добирается до поверхности подключения. Никогда не позволяйте бандлеру инлайнить или преобразовывать его и всегда поставляйте sdk.wasm и wasm_exec.js из одной сборки; клей ABI-спарен с тулчейном Go, который скомпилировал wasm, и сборка SDK гейтится на их побайтовом совпадении.

Из исходников. Нужен 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 записывает о подключениях, читайте в модели угроз.

Подключение

Новое устройство начинает с сетевых умолчаний, а они оставляют сеанс клиент↔провайдер незапечатанным. Профиля производительности у устройства нет, пока вы его не зададите, поэтому PostQuantumEncryption — флаг за настройкой приложений «Постквантовое шифрование» (Post Quantum Encryption) — стартует выключенным, и клиент не открывает с провайдером никакого сеанса: трафик идёт стандартным ретранслируемым путём, где оператор может прочитать пакеты, которые ретранслирует. Чтобы запечатать сеанс, установите флаг через setPerformanceProfile. Каждая текущая сборка провайдера включает отвечающую сторону, и с включённым флагом клиент работает в режиме 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 на этом пути нет. Зачем существует разделение и как работает переподключение — в туре.

cgo (Windows, Linux)

Контракт 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++-обёртки против сборки для хоста и запускает его.

JavaScript (wasm/веб)

import { URNetwork } from "@urnetwork/sdk";

const sdk = await URNetwork.init({
  wasmUrl: "/wasm/sdk.wasm",
  wasmExecUrl: "/wasm/wasm_exec.js",
});

Wasm регистрирует свои экспорты на globalThis, поэтому держите один экземпляр модуля на страницу. init идемпотентен внутри модуля (второй вызов возвращает тот же экземпляр), но две копии загрузчика — скажем, два бандла или два фрейма, делящие один realm, — гоняются за одними и теми же глобалами. Инициализируйте один раз в синглтоне уровня модуля, никогда — внутри жизненного цикла компонента.

Примеры с сокетами используют sdk.createPlatformDeviceRemote({...}) — DeviceRemote, говорящий по device-RPC с размещённым Device через WebSocket. Получите конфигурацию подключения и фактический идентификатор экземпляра этого размещённого Device у своего хостинг-сервиса:

const device = sdk.createPlatformDeviceRemote({
  apiUrl: "api.bringyour.com",
  platformUrl: "connect.bringyour.com",
  byJwt, proxyUrl,
  signedProxyId,   // HMAC auth token, not the JWT — see the tour
  instanceId,      // the actual hosted Device instance, not a fresh UUID
});

const unsub = device.addConnectLocationChangeListener((loc) => {
  console.log("location:", loc?.name);
});
device.setConnectLocation({ bestAvailable: true });

createPlatformDeviceRemote бросает исключение, если загруженный wasm старше биндинга DeviceRemote; это симптом старого npm-тега.

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

Поднимите устройство и проверьте выход: любая проверка «какой у меня 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 и браузер.