# Un recorrido por el SDK de URnetwork

Esta es la inmersión a fondo detrás de los
[primeros pasos](/docs/getting-started-sdk): cómo está montada cada capa de
bindings, los valores por defecto que trae y los bordes afilados que
conocemos. Todo corre contra la plataforma alojada de URnetwork. Tú traes
credenciales, no infraestructura. El modelo de objetos (dispositivo, API,
view controllers) está en [/docs/overview](/docs/overview) y
[/docs/api](/docs/api).

Un solo núcleo Go implementa el cliente entero: transporte, contratos, ruta
de paquetes. Cada artefacto (AAR, xcframework, wasm, biblioteca c-shared) es
un binding de ese mismo núcleo, y por eso el comportamiento es idéntico
entre plataformas, y por eso los artefactos son grandes (cada uno lleva el
runtime de Go). El núcleo lleva también reglas de producto, no solo
paquetes. Solana Pay vive aquí (`CreatePaymentReference`,
`BuildSolanaPaymentUrl`) porque una vez no fue así: la app web acuñaba la
referencia de pago como un uuid hexadecimal donde Solana Pay exige una
pubkey base58 de 32 bytes, así que un cliente podía pagar y no ser
emparejado nunca de vuelta con su cuenta, y Android llevaba codificados el
importe y la dirección del comercio. Reimplementar una regla que las apps ya
siguen es la clase de bug que esta capa existe para prevenir.

## Móvil

### DeviceLocal, y la división de procesos de app en Apple

`DeviceLocal` es la cosa real, el motor cliente en marcha: posee el
transporte, el estado de contratos (la contabilidad de URnetwork de los
bytes que una cuenta puede mover) y la ruta de paquetes. `DeviceRemote` es
un cliente de un `DeviceLocal` que vive en otro proceso o en la plataforma,
con la misma superficie de API: un mando a distancia del dispositivo real.
En Android un solo proceso aloja la UI y el `VpnService`, así que la app
sostiene el `DeviceLocal` directamente.

Apple ejecuta el manejo de paquetes de VPN en su propio proceso
NetworkExtension en sandbox (toda VPN de iOS/macOS se divide así), así que
el SDK se divide con él:

- La extensión `NEPacketTunnelProvider` posee el `SdkDeviceLocal` y llama a
  `setRpcServer(serverPem, clientCertPem, hostPort)` sobre él para arrancar
  el listener.
- El proceso de la app crea un `SdkDeviceRemote` y se acopla por una
  conexión **device-RPC con mTLS en loopback**: el
  `setRpcServer(clientPem, serverCertPem, hostPort)` en espejo, cada lado
  nombrando primero su propia identidad y segundo el certificado del par.
  Device-RPC es el protocolo propio del SDK: llamadas a métodos más flujos
  de suscripción sobre el net/rpc de Go, no gRPC. El TLS mutuo en loopback
  no es ceremonia. Localhost es alcanzable por todo proceso y usuario local,
  y solo quien posee los PEM correspondientes puede acoplarse.
- **La app acuña el material de claves, no la extensión.**
  `SdkGenerateDeviceRpcKeyMaterial()` devuelve un par de claves de servidor
  y cliente autofirmado nuevo por sesión de VPN; la app pone ambos PEM y el
  host:port en la **configuración de proveedor** del
  `NETunnelProviderProtocol`, que es como la extensión los recibe cuando el
  túnel arranca. No hay contenedor de app group implicado.
- Vigila `RemoteChangeListener`: la primera vez que `remoteConnected` pasa a
  verdadero ese material queda probado, así que persístelo como
  último-bueno-conocido y reaplícalo en el siguiente arranque. Eso es lo que
  permite a la app acoplarse a una extensión ya en marcha.

Las reconstrucciones son rutina: la extensión reconstruye su dispositivo
cada vez que el túnel se reinicia, porque el usuario conmuta la VPN, el
sistema reinicia la extensión, o iOS la mata por exceder el límite de
memoria de extensiones. Cablea ese reacople (acuñar, arrancar el túnel,
`setRpcServer`, reaplicar tu propio estado de UI) desde el primer día. Todo
lo que puedes hacer sobre un `DeviceLocal` lo puedes hacer sobre un
`DeviceRemote`; el remoto reenvía las llamadas y reproduce las
suscripciones a través de las reconexiones. Dos niveles: una **reconexión**
simple (el mismo dispositivo, el enlace RPC se cayó y volvió) restablece
las suscripciones por ti; una **recreación** (una instancia de dispositivo
nueva debajo) significa ejecutar esa secuencia otra vez. Una trampa:
`DeviceRecreatedListener` se dispara con un cambio de *generación de
dispositivo*, y solo la ruta RPC alojada en la plataforma estampa una, así
que en loopback dirige el reacople desde tu propio ciclo de vida del túnel
y `RemoteChangeListener`.

### IoLoop (solo Android)

`Sdk.newIoLoop(device, detachedFd)` es la bomba de paquetes:

- Pasa un fd **desacoplado y no bloqueante**
  (`ParcelFileDescriptor.detachFd()`). Tras esa llamada, **Go posee el fd y
  lo cerrará**; no vuelvas a envolverlo ni cerrarlo desde Java. Dos dueños
  significan doble cierre: los números de fd se reciclan de inmediato, así
  que un cierre extraviado desde Java puede pisar el descriptor sin
  relación que reciba el número después, corrupción lejos de la causa.
- El bucle bombea ambas direcciones (tun→dispositivo y dispositivo→tun)
  dentro de Go, lo que evita cruces JNI y copias de búfer por paquete. Por
  eso la API toma un fd crudo en lugar de exponer métodos de
  lectura/escritura.
- Cierra el IoLoop (no el fd) para parar; el callback de finalización se
  dispara cuando el bucle sale. Si se dispara sin que lo pidieras, porque
  el fd del tun llegó a EOF o a un error o el dispositivo se apagó, trátalo
  como "el túnel ya no está": termina la sesión del `VpnService`, y luego
  restablécela o muestra desconectado. El callback llega en un hilo del
  SDK, y Go cierra el fd a la salida; no lo toques nunca desde el gestor.

Apple no usa IoLoop: el proveedor del túnel mueve los paquetes por
`packetFlow`, la única interfaz que ofrece NetworkExtension. Toda VPN de iOS
paga ese cruce, y sus lecturas por lotes amortizan el coste.

### Los modos de compartir, y por qué importa el material de claves

`SetProvideMode` controla si el dispositivo ofrece capacidad a la red. El
valor por defecto es apagado: un dispositivo recién construido tiene el modo
de compartir en "none" y no ofrece nada hasta que fijes un modo, así que
incrustar el SDK nunca comparte en silencio el ancho de banda de tus
usuarios. Dos modos hacen algo hoy: **public**, y **network**, que limita el
compartir a otros dispositivos de la misma cuenta de URnetwork. El enum del
protocolo lleva más valores, pero la plataforma resuelve todo par fuera de
tu red a public, así que la elección real es apagado, mis propios
dispositivos, o cualquiera.

Compartir es donde importa la **persistencia del material de claves**: el
`DeviceLocalKeyMaterial` que pasas en la construcción *es* la identidad de
proveedor del dispositivo, una semilla de clave de cliente más el
certificado y la clave del TLS de compartir. Persístelo en el almacenamiento
seguro de la plataforma (Keystore, Keychain) y pasa el mismo material en
cada arranque, o la red ve un proveedor nuevo de fábrica cada vez y pierde
el historial de fiabilidad que la selección prefiere. El material efímero
(`null`) vale para clientes puros.

Si muestras el compartir, declara el intercambio: el tráfico de desconocidos
sale por la IP del usuario, y la posición de proveedor ve IPs de destino y
SNI de TLS (como un ISP), aunque por defecto no la IP real del usuario de
origen. La seguridad del proveedor está diseñada en el motor. La capa
ip_security de código abierto inspecciona la propia salida del proveedor y
descarta el tráfico de clase DMCA y de clase CFAA antes de que salga. El
veredicto es un paquete descartado, sin destino, dominio ni contenidos
registrados en ninguna parte. Una coincidencia de firma de BitTorrent emite
además una señal de abuso al operador que lleva solo el id de dispositivo
del par y un booleano, y el operador no incluye hoy ningún gestor para
ella; los descartes de cifrado opaco son silenciosos. Los proveedores
participan en el protocolo UR; [ur.xyz](https://ur.xyz) documenta las
recompensas.

### Notas de runtime

- El ritmo del GC se ajusta por sistema operativo automáticamente: un
  factor de ritmo de 10 en iOS (los límites de memoria de las extensiones
  son brutales) frente a 50 en Android y 100 en el resto. No lo fijas tú.
- `memoryTargetByteCount` es un mando distinto: un presupuesto de bytes que
  el dispositivo *reparte* para dimensionar sus propios búferes, dns 2 :
  cliente 14 : proveedor 4, con la parte del proveedor respaldando al par
  del cliente mientras el compartir está apagado (por defecto 20 MB). Ni un
  tope ni un ajuste del GC: el límite blando de huella de todo el proceso
  es el `SetMemoryLimit` aparte, y la muerte dura es el límite de
  extensiones del sistema. Fija el objetivo por debajo de él; mantén la
  extensión casi sin lógica.
- El bind de gomobile está vigilado en la compilación, y el mecanismo
  importa: gobind omite en silencio lo que no puede enlazar, dejando solo
  un comentario `// skipped` en las fuentes generadas, así que la
  compilación hace grep de esas fuentes y **falla ante cualquier omisión
  que no esté en una lista de permitidos explícita**. Las omisiones
  permitidas son internas (payloads gob del RPC, la superficie
  proxy/plataforma, formas `uint64`/`[][]byte` que gomobile no puede
  expresar): deliberadas, no deriva.

## JavaScript

### Por qué no hay DeviceLocal en wasm

Una página de navegador no puede poseer una interfaz tun, así que un
`DeviceLocal` en wasm no tendría nada que bombear. La capa JS trae solo las
mitades del lado cliente: la superficie de API, los view controllers y
`DeviceRemote`, un *cliente* de dispositivo completo cuyo dispositivo vive
en otra parte. La página sostiene el plano de control (estado de conexión,
ubicaciones, estadísticas, cuenta) mientras la ruta de tráfico la consume lo
que pueda usar el proxy alojado. De ahí las dos fábricas:
`createProxyDevice` es el modelo fino, que resuelve URLs de proxy alojadas y
deja el tráfico a lo que las consuma (una configuración de proxy de
extensión, un agente de fetch); `createPlatformDeviceRemote` es el modelo
grueso, un `DeviceRemote` real con toda la superficie de listeners y view
controllers sobre un dispositivo que la plataforma aloja por ti.

### La autenticación signedProxyId

`createPlatformDeviceRemote` abre un websocket de device-RPC a
`wss://<proxy>/device-rpc`. Ese websocket **no** se autentica con el JWT de
cuenta (`byJwt`, el token bearer del login de URnetwork; el "by" es una
herencia de BringYour). Se autentica con `signedProxyId`, el `auth_token`
que el endpoint `/network/auth-client` de la plataforma devuelve junto a la
URL del proxy. Es separación de alcances deliberada: el id de proxy firmado
autoriza exactamente un websocket y no lleva autoridad de cuenta, así que el
token amplio nunca viaja por el socket del plano de datos. Trata
(`proxyUrl`, `signedProxyId`) como una sola credencial. Pídelos, pásalos y
refréscalos juntos, y vuelve a pedir el par cuando el proxy rechace el
socket en lugar de cachear una pieza.

### El patrón de listeners y los view controllers

Toda suscripción sigue la misma forma: `add*ChangeListener(fn)` devuelve una
**función de desuscripción**. Consérvala y llámala en el desmontaje. En
React, devuélvela:

```js
useEffect(() => {
  const unsub = device.addConnectChangeListener(setConnectEnabled);
  return unsub;
}, [device]);
```

Eso sobrevive a los efectos doblemente invocados del modo estricto de React
(añadir, desuscribir, añadir), dejando exactamente una suscripción viva. La
superficie de view controllers (conexión, ubicaciones, dispositivos,
contratos, acciones de bloqueo) también está enlazada dentro del wasm,
colgando del dispositivo, como en `device.openConnectViewController()`, así
que una app web reutiliza la misma lógica de presentación que las apps
móviles en lugar de rederivar el estado desde listeners crudos.

### Cómo lo usa ur.io

La superficie `/app` de ur.io es el consumidor de referencia: el wasm se
**carga en diferido** solo cuando el usuario llega a una superficie de
conexión, así que las páginas de aterrizaje nunca pagan el coste de ~43 MB,
y hay un `DeviceRemote` por pestaña. Los exports son globales a la página,
así que `init` está detrás de un singleton.

La extensión de navegador empaqueta el mismo wasm pero hoy no lo instancia:
mueve la conectividad a través del proxy de la plataforma más las APIs de
proxy propias del navegador, y consume el **otro** punto de entrada del
paquete npm, `@urnetwork/sdk-js/react`, hooks de API basados en `fetch`
plano y tipos generados que no necesitan wasm. Si solo quieres la superficie
REST, importa eso y no llames nunca a `init`: el cargador resuelve la URL
del wasm en tiempo de ejecución y no vía un
`new URL(..., import.meta.url)` estático, así que los bundlers no lo emiten
para los consumidores que no lo usan.

## cgo

### La división con el demonio

Las apps de Linux y Windows de URnetwork que se distribuyen están
construidas sobre esta capa, y ambas usan el mismo patrón de dos procesos,
la forma a copiar para cualquier integración cgo que toque un dispositivo
tun. El SDK trae el motor y la API de C; el demonio es tuyo, con esas dos
apps publicadas como referencias de código abierto:

- Un **proceso root/de servicio** (unidad de systemd, servicio de Windows)
  ejecuta el `DeviceLocal` y posee la interfaz tun.
- El **proceso de UI sin privilegios** ejecuta un `DeviceRemote` y se acopla
  por device-RPC con mTLS en loopback; la dirección por defecto del SDK es
  `127.0.0.1:12025`.
- Los PEM del mTLS se entregan a la UI por un canal que el propio sistema
  operativo autoriza: un **socket unix comprobado con `SO_PEERCRED`** en
  Linux (comprobado en el accept, antes de leer ningún frame), una **tubería
  con nombre** en Windows. Ese handshake, no el puerto TCP, es la frontera
  de autorización real; el mTLS en loopback solo mantiene a los demás
  usuarios locales fuera del puerto.

### Semántica del envoltorio RAII (`urnetwork_sdk.hpp`)

- Los handles se envuelven en tipos con propiedad cuyo destructor llama a
  `urnet_release`. Pero **liberar no es cerrar/parar**: soltar el último
  envoltorio no detiene un dispositivo ni cierra una conexión. Llama antes a
  `*_close` / `*_stop` (los `.close()` / `.stop()` del envoltorio), y deja
  luego que el envoltorio libere. Varios handles pueden referirse a un mismo
  objeto vivo, así que una salida de ámbito no debe matar jamás en silencio
  una sesión viva.
- Las suscripciones vuelven como `urnet::Sub`; el destructor desuscribe.
- Los errores afloran como excepciones `urnet::Error` que llevan el texto de
  `out_error`.

Una regla dura, lo contrario de lo que la gente espera:

```cpp
sub = device.addConnectChangeListener([&](bool enabled) { /* ... */ });
sub.close();   // returns immediately; a callback may still be running
// do NOT free what that lambda captured here
```

Las listas de listeners son copy-on-write, así que desuscribir quita la
entrada y vuelve. Es deliberado (a un callback se le *permite* quitarse a sí
mismo desde dentro de sí mismo), pero significa que un callback puede seguir
corriendo en otro hilo después de que el `Sub` haya desaparecido. Destruir
el estado que ese listener capturó es el crash real; mantenlo vivo más allá
del desmontaje con un `shared_ptr` o una bandera que el callback compruebe.

Para la disciplina de fugas, toma una instantánea de
`urnet_live_handle_count()` antes de un escenario, ejecuta
construir/usar/cerrar/liberar y comprueba que vuelve al valor de partida.
`cgo/smoke` hace exactamente esto y es el patrón a copiar.

### Qué está enlazado de verdad

La ABI de C se *genera* desde la superficie Go, y el generador escribe
`cgo/coverage_report.txt`: cada símbolo exportado, y para cada uno que no
cruzó, la razón (contextos de Go, entrañas de `net.Conn`, parámetros de
función, llamadas con propiedad de pool, tipos gob del RPC). Lee ese
archivo, no el código Go, para responder "¿esto se puede llamar desde C?".
Un export nuevo de Go llega a la cabecera solo cuando corre el generador,
así que la cabecera viaja junto a la biblioteca con la que se cortó. Fíjalos
juntos.

### Compatibilidad de protocolo

El protocolo de cable del device-RPC está **fijado por versión
(`DeviceRpcVersion`, actualmente 1) y aplicado** por el local en cada
sincronización. Deliberadamente *no* es la versión de la release: las
mitades alojadas se despliegan por separado, así que atarlas rechazaría a
todos los navegadores tras un despliegue del servidor. Un desajuste no lanza
excepción. El remoto se queda sin sincronizar y reintenta, lo que parece
idéntico a "el demonio no está en marcha". Distínguelos con
`GetRemoteConnected()` más `GetSyncError()`: un error de sincronización
vacío significa aún no alcanzable, mientras que
`"device rpc version mismatch: ..."` o `"device instance mismatch: ..."` es
un rechazo que reconectar no arreglará jamás. Distribuye el demonio y la UI
de la misma release del SDK.

## Transversal

### Ajustes que llevan política

Estos flags sostienen decisiones de producto, no afinado, y sus valores por
defecto son la postura de privacidad que reciben tus usuarios. Lo que trae
un dispositivo de fábrica:

| Ajuste | Valor por defecto | Efecto | Coste principal |
|---|---|---|---|
| `SetPerformanceProfile` | nil (auto) | las ventanas de calidad y velocidad corren en paralelo; el tráfico sale por varios proveedores a la vez (normalmente 3–8), con afinidad por sitio | fijar un `WindowType` estrecha a una ventana |
| `AllowDirect` | desactivado | conserva el salto anonimizador, así que ningún proveedor ve la IP real del usuario | activado: más rendimiento, y el proveedor ve la IP real del usuario |
| `PostQuantumEncryption` | activado | sella la sesión cliente↔proveedor, así que el operador retransmite bytes que no puede leer | un proveedor con el que no puede sellar se omite, no se usa sin sellar |
| `SetRouteLocal` | permitir | el tráfico recurre a la ruta local cuando el túnel se cae | no permitir es el kill switch: el tráfico se detiene |
| `SetProvideMode` | none | el dispositivo no ofrece capacidad a la red | compartir en public o network saca el tráfico de otros por la IP del usuario |

Salvedades que acompañan a la tabla:

- `AllowDirect` es el ajuste de velocidad, y la IP que expone es exactamente
  el salto que quita. Las apps lo muestran invertido como "Anonimización
  fuerte" (Strong Anonymization), activada por defecto, y está forzado a
  desactivado en los perfiles de dispositivo alojados fije lo que fije quien
  llama. No lo presentes nunca a tus usuarios como velocidad gratis;
  preséntalo como el intercambio que es.
- `PostQuantumEncryption` es la sesión cliente↔proveedor de extremo a
  extremo que las apps publican como "Cifrado poscuántico" (Post Quantum
  Encryption), activada por defecto desde el arranque y lista en los
  proveedores: toda compilación actual de proveedor habilita el lado que
  responde. Mientras está activada, 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. El coste es de disponibilidad, no de
  confidencialidad. Desactívala y el tráfico vuelve a poder tomar la ruta
  estándar.
- `SetRouteLocal` es la primitiva del kill switch ("permitir tráfico
  local"). Desactívala, y dale a los usuarios el conmutador que tiene toda
  app de URnetwork, para que el tráfico se detenga en lugar de recurrir a la
  ruta local cuando el túnel se cae.

La ceguera del proveedor ante la identidad de tu usuario es incondicional;
la ceguera del operador ante el contenido también, por defecto. Dos
propiedades, ambas ciertas en un perfil de fábrica, que es lo que hace
exacto decir que ninguna parte por sí sola reúne la identidad de tu usuario
y su actividad. Cambia cualquiera de los dos flags y cambias la propiedad
correspondiente por otra cosa, no la ganas.

Los tres modos que esos dos flags seleccionan, enunciados para los usuarios
que enrutas:

| Modo | El operador ve | El proveedor ve | Cómo se obtiene |
|---|---|---|---|
| **Retransmitido sellado** | conexión de cuenta/origen, asociación con proveedores, texto cifrado y tiempo/volumen | tráfico de destino y un id de dispositivo/contrato, **no** la IP real del usuario | el valor por defecto: `PostQuantumEncryption` activado, `AllowDirect` desactivado |
| **Retransmitido estándar** | conexión de cuenta/origen, asociación con proveedores, destinos interiores y bytes de los paquetes | tráfico de destino y un id de dispositivo/contrato, **no** la IP real del usuario | solo si desactivas `PostQuantumEncryption` |
| **Directo** | menos intervención en la retransmisión | **la IP real del usuario** y el tráfico de destino | opcional: `AllowDirect` activado (forzado a desactivado en los perfiles alojados) |

El valor por defecto ya trae la primera fila sin nada que fijar. La segunda
exige desactivar `PostQuantumEncryption`: mientras está activada, el cliente
omite todo proveedor con el que no puede sellar en lugar de caer a esa fila.
No hay API por conexión que informe en qué fila acabó una sesión dada. El
[modelo de amenazas](/docs/threat-model) trabaja estas filas frente a
adversarios con nombre, y es explícito sobre dónde falla cada una.

### Reglas de hilos

- Los objetos que no son de vista (`DeviceLocal`, `DeviceRemote`, `Api`,
  espacios de red) son seguros en concurrencia. Llámalos desde cualquier
  hilo.
- **Los view controllers son de un solo hilo** salvo que uno documente lo
  contrario. Maneja cada uno desde un hilo (normalmente tu hilo de UI). El
  JS del navegador lo satisface gratis; muerde en Android, Apple y cgo,
  donde los callbacks llegan en hilos gestionados por Go.
- Los callbacks se disparan en hilos arbitrarios gestionados por el SDK.
  Reencamínalos a tu hilo de UI antes de tocar la UI, y no bloquees nunca en
  uno: un `DeviceRemote` serializa sus callbacks por un único canal con
  búfer, así que un listener lento aplica contrapresión a todas sus
  suscripciones.

### Tamaños de los artefactos

Planifica los presupuestos de descarga y empaquetado alrededor de estos:

| Artefacto | Tamaño (comprimido donde se indica) |
| --- | --- |
| AAR de Android | ~37 MB |
| xcframework de Apple (zip) | ~116 MB |
| wasm de JS | ~43.5 MB |
| c-shared de Linux (zip) | ~25 MB |
| c-shared de Windows (zip) | ~21 MB |

### Estabilidad de la API

Las versiones van por fecha (`vYYYY.M.D-<code>`); no hay contrato SemVer
todavía, y el paquete npm es beta y se republica cada noche. Trata las tres
superficies como en movimiento hasta un 1.0: fija versiones exactas, lee las
notas de cada versión en cada subida y mantén los artefactos emparejados
(demonio y UI, extensión y app) en una misma release.
