Primeros pasos con el SDK de URnetwork
Instala el SDK con el gestor de paquetes de tu lenguaje y después explora los ejemplos por lenguaje y la guía de sockets.
Esta guía lleva a un desarrollador de un proyecto vacío a una primera conexión: consigue un JWT de cuenta, instala el binding de tu plataforma, levanta un dispositivo y confirma que el tráfico fluye. El SDK es un módulo Go, github.com/urnetwork/sdk, expuesto a través de cuatro bindings:
- Android — el núcleo compilado a código nativo y enlazado para Kotlin/Java, distribuido como un AAR.
- iOS / macOS — la misma vía de gomobile, enlazada para Swift, distribuida como un xcframework.
- cgo (Windows, Linux) — el núcleo como biblioteca c-shared detrás de una ABI de C, con una cabecera C++17 por encima. Cualquier lenguaje con FFI llega a él por aquí.
- JavaScript (wasm/web) — el núcleo compilado a WebAssembly y ejecutado en la página, manejado por tu framework de navegador.
Dan frente a un solo núcleo, así que el comportamiento coincide entre plataformas — pero no son intercambiables, y las diferencias deciden tu arquitectura y no solo tu sintaxis. Lee Qué binding elegir antes de elegir uno, y el recorrido del SDK para ver cómo está construido cada uno.
URnetwork usa dispositivos de salida gestionados por miembros. Los proveedores no reciben las IPs de origen de tus usuarios en las rutas retransmitidas. Los dispositivos construidos sobre el SDK también pueden cifrar el tráfico hasta el proveedor, pero solo cuando activas PostQuantumEncryption; un dispositivo nuevo arranca con ese flag desactivado. Lee Cómo funciona URnetwork para el modelo completo.
Qué necesitas
Los cuatro bindings dan frente al mismo núcleo sobre el que están construidas las apps oficiales de URnetwork: el dispositivo (el motor de conexión), el cliente de la API, el modelo de configuración de espacio de red (con qué despliegue de la plataforma hablas, con qué endpoints y flags) y los view controllers, objetos sin interfaz de estado más eventos para las pantallas comunes, a los que puedes enlazar tu UI o ignorar. La API del operador que hay detrás está documentada en /docs/api. Estás construyendo tu propia app sobre la plataforma alojada de URnetwork: la API, los relés y los proveedores son el servicio en vivo, y tus usuarios traen o crean cuentas.
Mínimos por plataforma, por binding:
- Android — nivel de API 24 o más reciente. El paquete Java es
com.bringyour.sdk. - iOS / macOS — iOS 16.0 y macOS 13.5.
- cgo — Ubuntu 22.04+ (glibc 2.35+) o Windows 10+, amd64 y arm64.
- JavaScript — solo navegador.
- Compilar desde el código — Go 1.26+.
El SDK es MPL-2.0 (Mozilla Public License): enlázalo libremente en apps de código cerrado. El copyleft a nivel de archivo solo obliga a compartir los cambios a los propios archivos del SDK.
Qué binding elegir
Los bindings nativos pueden poseer la ruta de paquetes del dispositivo. Una página de navegador no puede poseer una interfaz TUN del sistema operativo; su Device de JavaScript controla un dispositivo remoto. El SDK de JS actualizado también expone sockets de aplicación a través de un Device remoto con capacidad de sockets, incluidos TCP, UDP, TLS/DTLS y la interfaz Direct Sockets de JavaScript. Esto enruta esas conexiones de aplicación sin convertir la página del navegador en una VPN del sistema operativo.
| Android | iOS / macOS | cgo | JavaScript | |
|---|---|---|---|---|
| Escribes en | Kotlin / Java | Swift | C++ (o cualquier FFI) | JS / TypeScript |
| El núcleo llega como | código nativo, enlazado con gomobile | código nativo, enlazado con gomobile | biblioteca c-shared, ABI de C | wasm en la página |
| Ejecuta la ruta de paquetes | sí, en el proceso de tu app | sí, en la NetworkExtension | sí, en un demonio que escribes tú | no |
| Modelo de procesos | un solo proceso | app + extensión, mTLS por loopback | UI + servicio privilegiado, mTLS por loopback | solo la página |
| Artefacto | AAR de ~37 MB | xcframework de ~116 MB | biblioteca de ~21–25 MB | wasm de ~43.5 MB |
Lo que eso te cuesta, en el orden en que te va a morder:
- Android es el más sencillo: un solo proceso sostiene la UI y el
VpnService, así que tu app posee elDeviceLocaldirectamente eIoLoopbombea el fd del tun dentro de Go sin ningún cruce de JNI por paquete. - iOS / macOS es el mismo binding con una forma más difícil. Toda VPN de Apple ejecuta el manejo de paquetes en un proceso aislado aparte, así que el dispositivo vive en la extensión y tu app lo maneja en remoto. Cuenta desde el primer día con el ciclo de vida de reacoplamiento y con el límite de memoria de la extensión — no es un caso límite; el sistema operativo reinicia ese proceso de forma rutinaria.
- cgo es el más portable y el menos ergonómico. La ABI son handles y JSON en lugar de objetos tipados, que es lo que permite que Rust, Python y C# lleguen a ella; la cabecera C++17 es el único lugar donde se invirtió en ergonomía. Además, el demonio privilegiado lo escribes tú.
- JavaScript cambia la ruta de paquetes por alcance. Obtienes la superficie de la API, los view controllers y
DeviceRemotecontra un dispositivo alojado en otro lugar — y una carga de 43 MB que cargar en diferido.
Los hilos son donde los cuatro convergen en una sola regla: los objetos que no son de vista son seguros para la concurrencia, los view controllers no. El JS de navegador lo obtiene gratis; los otros tres no. Consulta el recorrido para los detalles.
Iniciar sesión
Cada fragmento de abajo toma un byJwt: el JWT de cuenta, un token de inicio de sesión firmado que la API de URnetwork emite cuando una cuenta se autentica. No hay un sistema aparte de claves de API.
Tu app ejecuta un flujo de login una vez a través del cliente de API del SDK. authLogin informa de los métodos de autenticación de un identificador; authLoginWithPassword lo completa (authVerify termina un código enviado por correo o SMS), y la autenticación con cartera y networkCreate son puntos de entrada paralelos. La autenticación con cartera es solo por firma: envías wallet_address, wallet_message y wallet_signature, nunca una clave, y ninguna superficie de URnetwork pide la clave privada ni el mnemónico de una cartera. networkCreate sin ningún método de autenticación es la vía de la cuenta instantánea: acuña una cuenta permanente y devuelve una seedphrase, la frase de recuperación propia de URnetwork para esa cuenta, generada en el servidor y entregada exactamente una vez. Muéstrasela al usuario en ese momento, o la cuenta no tendrá vía de recuperación.
Toda vía devuelve by_jwt. Persístelo, llama a setByJwt y pásalo al constructor del dispositivo. No escribes código de renovación; el gestor de tokens del Api rota el JWT. Trata un fallo duro de autenticación como "ejecuta el login otra vez".
La cuenta es también la unidad de facturación: la cuenta cuyo JWT sostiene el dispositivo es aquella cuyo plan se mide. El gratuito es una cuota de datos diaria, Pro una mensual grande; números vigentes en ur.io/products. Plantea una cuenta por usuario final, que la vía de la cuenta instantánea hace sin fricción, en lugar de una cuenta embebida que agrupe el uso y la conducta de todos los usuarios en un solo actor. Cuando una cuenta se queda sin datos, la transferencia se detiene hasta que la cuota se renueva, así que di "sin datos" en tu UX, no un error de red genérico.
El SDK también posee el flujo del plan de pago, porque el cliente nunca debe nombrar su propio precio: registra una intención con createSolanaPaymentIntent (reference de createPaymentReference, plan "monthly" o "yearly"), toma amountUsd del resultado y pásalo a buildSolanaPaymentUrl. La ABI de C tiene la llamada de intención, aún no el constructor de la URL.
Instalar
Los artefactos precompilados son assets de versión en github.com/urnetwork/build. Las versiones van por fecha, vYYYY.M.D- (p. ej. v2026.7.22-999364023), no SemVer: fija una versión y actualiza deliberadamente, porque la versión dice cuándo se cortó, no si la API se movió.
Android. Descarga URnetworkSdk-.aar (más -sources.jar para la navegación en el IDE) y déjalos en un directorio que tu módulo Gradle ya escanee, p. ej.:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}iOS / macOS. Descarga URnetworkSdk.xcframework.zip, descomprímelo y referéncialo desde un paquete Swift local como un binaryTarget:
// Package.swift
targets: [
.binaryTarget(name: "URnetworkSdk", path: "URnetworkSdk.xcframework")
]Todos los tipos llevan el prefijo Sdk (SdkDeviceLocal, SdkNetworkSpace, ...). El xcframework lleva ios/arm64, iossimulator/arm64, macos/arm64 y macos/amd64; no hay slice Intel del simulador de iOS.
cgo (Windows, Linux). Bibliotecas c-shared, la misma capa sobre la que están construidas las apps de Linux y Windows de URnetwork que se distribuyen, con dos cabeceras curadas al lado:
libURnetworkSdk.so— Linux.URnetworkSdk.dll(+urnetwork_sdk.def) — Windows.urnetwork_sdk.h— la ABI de C plana.urnetwork_sdk.hpp— un envoltorio RAII C++17 de solo cabecera sobre ella, que requiere nlohmann/json en tu ruta de includes.
Cualquier cosa con una interfaz de funciones foráneas de C (Rust bindgen, Python ctypes, C# P/Invoke) se acopla a esta ABI de handles y JSON; la cabecera C++ es la única comodidad de lenguaje. Con MSVC, genera primero la biblioteca de importación desde el archivo .def:
lib /def:urnetwork_sdk.def /machine:x64 /out:URnetworkSdk.libJavaScript (navegador y Node). El paquete canónico es @urnetwork/sdk, que contiene el cargador, las declaraciones de TypeScript, el wasm_exec.js de Go y el WASM del SDK correspondiente. Usa una versión con capacidad de sockets.
npm install @urnetwork/sdk@nightly # then pin the version it resolved toEl WASM contiene el núcleo de red de Go y es una descarga grande. Sírvelo como asset estático (gzip o brotli, con caché agresiva) y cárgalo en diferido, solo cuando el usuario llegue a una superficie de conexión. No dejes nunca que un bundler lo incruste o lo transforme, y distribuye siempre sdk.wasm y wasm_exec.js de la misma compilación; el pegamento está emparejado por ABI con la toolchain de Go que compiló el wasm, y la compilación del SDK exige que los dos coincidan byte a byte.
Desde el código. Necesitas Go 1.26+, y para móvil make init primero: fija el gomobile exacto con el que se cortan los bindings e instala el checksec que ejecuta el target de Android. build_android también necesita ANDROID_NDK_HOME definido, porque elimina .comment con el llvm-objcopy del NDK. sdk/build-android.sh y sdk/build-ios.sh envuelven ambos. Desde 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 se apoya en la lista de omisiones de gomobile, así que un símbolo ausente es un desajuste de versiones, no un binding caído en silencio; gomobile no enlaza slices de structs (las listas cruzan como SdkStringList, SdkIdList, ...) ni context.Context.
Las evaluaciones de seguridad de terceros, y sus límites, están recogidas en el modelo de amenazas.
El permiso del túnel
El consentimiento del sistema operativo, y el túnel en sí, pertenecen a tu app. El SDK empieza en la capa de paquetes:
- En Android, el
VpnServicees tuyo: tu app lo declara, obtiene el consentimiento de VPN conVpnService.prepare()y llama aestablish(). El SDK toma el relevo en ese descriptor de archivo. - En las plataformas Apple, la división es un requisito de Apple, no del SDK: los túneles de paquetes corren en una NetworkExtension aparte con límites de memoria estrictos, así que el proveedor del túnel posee el dispositivo mientras el proceso de tu app se acopla a él en remoto.
- En un navegador, una página no puede poseer una interfaz de red, así que no hay permiso que pedir, y nada en la capa JavaScript tuneliza el tráfico de la propia página. Los dos modelos de dispositivo de JavaScript manejan en su lugar un dispositivo en la plataforma. La extensión de URnetwork cubre las pestañas del navegador; una app nativa cubre la máquina entera.
Para saber qué registra URnetwork sobre las conexiones, lee el modelo de amenazas.
Conectar
Un dispositivo nuevo arranca con los valores por defecto de la red, y esos dejan sin sellar la sesión cliente-proveedor. Un dispositivo no tiene perfil de rendimiento hasta que le pones uno, así que PostQuantumEncryption, el flag detrás del control "Cifrado poscuántico" (Post Quantum Encryption) de las apps, arranca desactivado, y el cliente no abre ninguna sesión con el proveedor: el tráfico toma la ruta retransmitida estándar, donde el operador puede leer los paquetes que retransmite. Para sellar, activa el flag con setPerformanceProfile. Toda compilación actual de proveedor habilita el lado que responde, y con el flag activado el cliente es fail-closed: no lleva datos de aplicación en claro, y omite todo proveedor con el que no puede sellar en lugar de usarlo sin sellar. AllowDirect está desactivado por defecto; es el ajuste de velocidad opcional que quita el salto anonimizador y le entrega a ese proveedor la IP real de tu usuario, las apps lo muestran invertido como "Anonimización fuerte" (Strong Anonymization), y los perfiles de dispositivo alojados lo fuerzan a desactivado. La protección contra fugas te toca cablearla a ti: el kill switch (interruptor de corte) es la primitiva setRouteLocal, un dispositivo arranca con el enrutamiento local permitido, y setRouteLocal(false) hace que el tráfico se detenga en lugar de recurrir a la ruta local cuando el túnel está caído. El SDK no incrusta analítica ni informes de fallos; sus únicas conexiones son los endpoints de la plataforma y los relés y proveedores que usa el dispositivo.
Android
Orden de arranque: crea un NetworkSpaceManager apuntado a almacenamiento privado de la app (el directorio guarda estado local, credenciales incluidas), crea el espacio de red y luego pon el JWT de cuenta en su cliente de API. La clave es un nombre de host más un nombre de entorno; producción es ("ur.network", "main"), y updateNetworkSpace es lo que crea uno, mientras que getNetworkSpace solo relee un espacio que ya existe, así que devuelve null en una instalación recién hecha. Las URLs derivan de la clave (https://api., wss://connect.; un entorno distinto de main prefija el servicio), pero la derivación prefiere migrationHostName, que producción fija en bringyour.com: las apps distribuidas llegan a api.bringyour.com, y api.ur.network no resuelve. Después crea el dispositivo y entrégale el descriptor de archivo del túnel:
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 toma posesión del fd desacoplado y bombea paquetes en ambas direcciones hasta que lo cierras. No vuelvas a tocar el fd desde Java después de desacoplarlo.
Tres argumentos del constructor merecen cuidado. instanceId: genera un Sdk.newId() en el primer arranque, persístelo y reutilízalo durante la vida de la instalación (un dispositivo vivo por proceso); un id nuevo en cada arranque añade una entrada fantasma a la lista de dispositivos de la cuenta. keyMaterial: null vale para un cliente puro, pero si el dispositivo va a proveer capacidad alguna vez, persiste getKeyMaterial() en el almacenamiento seguro de la plataforma. Es la identidad de proveedor del dispositivo, y perderla reinicia el historial de fiabilidad del proveedor. memoryTargetByteCount: un presupuesto de bytes contra el que el dispositivo dimensiona sus búferes y el ritmo del GC; elige uno que quepa en el techo real de tu proceso (ver el recorrido). DeviceLocal va dentro del proceso: si tu proceso muere, el túnel muere con él, así que ejecuta el VpnService como servicio en primer plano y recrea el dispositivo al reiniciar con el mismo instanceId y el mismo material de claves.
iOS / macOS
El NEPacketTunnelProvider posee el SdkDeviceLocal y el flujo de paquetes, mientras el proceso de la app se acopla a él como dispositivo remoto por 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)La app acuña el material de claves y los PEM del RPC (SdkGenerateDeviceRpcKeyMaterial) y se los entrega a la extensión en la configuración de proveedor del NETunnelProviderProtocol; la extensión lee rpc_server_pem, rpc_client_pem y rpc_listen_hostport de ahí. No hay app group en esta vía. Ver el recorrido para saber por qué existe la división y cómo funciona la reconexión.
cgo (Windows, Linux)
El contrato de la ABI, en corto:
- Los objetos son handles
uint64_topacos.urnet_release(h)libera el handle sin detener el objeto, así que llama antes a su*_close/*_stopdonde exista. - Las cadenas
char*devueltas pertenecen al llamador; libéralas conurnet_free_string. - Los datos estructurados cruzan la frontera como cadenas JSON UTF-8; los ids son cadenas UUID, los tiempos son milisegundos de época Unix (
0= ninguno). - Los callbacks se disparan en hilos arbitrarios gestionados por Go; reencamínalos a tu propio hilo. Sus cadenas y búferes viven solo durante la llamada, y los handles que te entregan son tuyos para liberar.
- Las llamadas que pueden fallar toman un
char** out_error; en caso de fallo se rellena con un mensaje que liberas conurnet_free_string. PasaNULLpara ignorar el texto.
La cabecera lleva una división por plataforma: urnet_new_io_loop, la bomba de fd que usa la app de Linux, está dentro de #if !defined(_WIN32) y no aparece en el .def de Windows. En Windows mueves los paquetes con urnet_device_local_send_packet y urnet_device_local_add_receive_packet.
Un ejemplo funcional de extremo a extremo vive en cgo/smoke; make smoke_hpp en sdk/cgo compila el test de humo del envoltorio C++ contra una compilación de host y lo ejecuta.
JavaScript (wasm/web)
import { URNetwork } from "@urnetwork/sdk";
const sdk = await URNetwork.init({
wasmUrl: "/wasm/sdk.wasm",
wasmExecUrl: "/wasm/wasm_exec.js",
});El wasm registra sus exports en globalThis, así que ejecuta una instancia del módulo por página. init es idempotente dentro de un módulo (una segunda llamada devuelve la misma instancia), pero dos copias del cargador, digamos dos bundles o dos frames compartiendo un realm, compiten por los mismos globales. Inicializa una vez en un singleton a nivel de módulo, nunca dentro del ciclo de vida de un componente.
Los ejemplos de sockets usan sdk.createPlatformDeviceRemote({...}), un DeviceRemote que habla device-RPC con un Device alojado por un WebSocket. Obtén de tu servicio de alojamiento la configuración de la conexión y el ID de instancia real de ese Device alojado:
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 lanza una excepción si el wasm cargado es anterior al binding de DeviceRemote; ese es el síntoma de una etiqueta npm vieja.
Comprueba que funciona
Levanta el dispositivo y comprueba la salida: cualquier comprobación de "cuál es mi IP" a través del túnel debería informar ahora de la dirección de un proveedor, no la de la máquina. El mismo estado es visible en código y en la cuenta: los listeners se disparan según cambia la conexión (el fragmento de JavaScript de arriba registra cada cambio de ubicación según llega), y el dispositivo aparece en la lista de dispositivos de la cuenta bajo el deviceDescription que pasaste. En la ABI de C, urnet_live_handle_count() informa de los handles vivos; comprueba en los tests de fugas que vuelve a su valor de partida.
Si algo falla
El soporte son los canales abiertos: issues en los repositorios (github.com/urnetwork), comentarios de producto en feedback.ur.io e informes de seguridad a [email protected] (política de divulgación en ur.io/vdp). Hoy no hay un nivel de soporte de SDK de pago. Dos atascos tienen explicación fácil: una transferencia detenida sobre un túnel que funciona suele ser una cuenta sin datos, y un cargador de JavaScript sin nada que cargar es la etiqueta npm latest. El recorrido cubre los modos de fallo que conocer antes de publicar.
Qué sigue
- Haz el recorrido del SDK. La arquitectura detrás de cada binding: divisiones de procesos, hilos, autenticación, reconexión y cómo dimensionar el objetivo de memoria.
- Cuéntales a tus usuarios a qué se unen. Si tu app enruta tráfico a través de URnetwork, la visión general es el relato canónico de la ruta y de lo que cada parte puede ver, y el modelo de amenazas es el registro completo que hay detrás.
- Mira las apps terminadas para ver la experiencia que tendrán tus usuarios: Android, iOS, macOS, Windows, Linux y el navegador.