Developers

Primeros pasos con el SDK de URnetwork

13 min de lecturaView as markdown ↗

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 tres capas de bindings: bindings gomobile para Android y Apple, una compilación WebAssembly para JavaScript, y bibliotecas c-shared con una ABI de C curada para todo lo demás. Para la arquitectura detrás de cada binding, lee el recorrido del SDK.

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 además cifran el tráfico hasta el proveedor por defecto. Lee Cómo funciona URnetwork para el modelo completo.

Qué necesitas

Las tres capas 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.
  • JavaScript — solo navegador.
  • cgo — Ubuntu 22.04+ (glibc 2.35+) o Windows 10+, amd64 y arm64.
  • 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.

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.

JavaScript. El paquete npm es @urnetwork/sdk-js, un cargador solo para navegador que descarga el wasm_exec.js de Go más el wasm del SDK y los instancia en la página. Instala la etiqueta nightly; se corta del SDK actual con el mismo esquema de versiones por fecha y trae el wasm. latest va meses por detrás y no lleva wasm alguno, así que un npm install a secas te deja un cargador sin nada que cargar.

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

El wasm ronda los 43 MB, el núcleo Go entero, así que no va a encoger, pero comprime bien: 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.

cgo. 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.lib

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 VM

build_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 VpnService es tuyo: tu app lo declara, obtiene el consentimiento de VPN con VpnService.prepare() y llama a establish(). 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. La sesión cliente-proveedor va sellada de fábrica: setPerformanceProfile sale con PostQuantumEncryption activado, el flag detrás del control "Cifrado poscuántico" (Post Quantum Encryption) de las apps, y toda compilación actual de proveedor habilita el lado que responde. 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. Desactívalo y el tráfico vuelve a poder tomar la ruta estándar. 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.

JavaScript

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

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

El wasm registra sus exports en window, 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.

Dos modelos de dispositivo:

  • sdk.createProxyDevice(...) — un cliente ligero de URLs de proxy alojadas, para cuando todo lo que necesitas es "una URL de proxy que salga por URnetwork".
  • sdk.createPlatformDeviceRemote({...}) — un DeviceRemote completo que habla device-RPC con un dispositivo alojado por un websocket, para una UI de conexión real (ubicaciones, listeners, estadísticas):
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 lanza una excepción si el wasm cargado es anterior al binding de DeviceRemote; ese es el síntoma de una etiqueta npm vieja.

cgo

El contrato de la ABI, en corto:

  • Los objetos son handles uint64_t opacos. urnet_release(h) libera el handle sin detener el objeto, así que llama antes a su *_close/*_stop donde exista.
  • Las cadenas char* devueltas pertenecen al llamador; libéralas con urnet_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 con urnet_free_string. Pasa NULL para 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.

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.