<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <title>Notas técnicas · Nob</title>
  <link>https://nuulm.com/es/notas</link>
  <atom:link href="https://nuulm.com/es/notas/feed.xml" rel="self" type="application/rss+xml"/>
  <description>Notas técnicas de Nob: lo que aprendí haciendo IP Rotator, celltrace, jukz, AccessScope y las demás herramientas, con los errores incluidos</description>
  <language>es</language>
  <lastBuildDate>Thu, 01 Oct 2026 12:00:00 +0000</lastBuildDate>
  <image><url>https://nuulm.com/og/notas.png</url><title>Notas técnicas · Nob</title><link>https://nuulm.com/es/notas</link></image>
  <item>
    <title>El comando que sí apaga el radio</title>
    <link>https://nuulm.com/es/notas/apagar-el-radio</link>
    <guid isPermaLink="true">https://nuulm.com/es/notas/apagar-el-radio</guid>
    <pubDate>Thu, 01 Oct 2026 12:00:00 +0000</pubDate>
    <description>Por qué activar el modo avión no siempre cambia la IP, qué hace de verdad el CGNAT del operador y cómo rotar la IP de un Android desde el PC por ADB, sin root (cmd connectivity airplane-mode)</description>
    <category>Android</category><category>ADB</category><category>CGNAT</category>
    <enclosure url="https://nuulm.com/og/notas/apagar-el-radio.png" type="image/png" length="0"/>
    <content:encoded><![CDATA[<p class="rv">La pregunta era concreta: ¿se puede cambiar la IP pública de un celular Android solo desde el PC, por ADB y sin root? Mucha gente da por hecho que activar el modo avión siempre trae una IP nueva, y yo quería comprobarlo con evidencia</p>
<h2 class="rv" id="s1">Lo que se cree</h2>
<p class="rv">La idea circula mucho: si quieres otra IP, activa el modo avión unos segundos y vuelve a desactivarlo. Y muchas veces funciona, por eso se repite. El problema está en el «siempre»: a veces la IP sale igual, y no siempre por la misma razón</p>
<h2 class="rv" id="s2">Qué pasa de verdad</h2>
<p class="rv">Para entenderlo hay que separar dos IP que se suelen confundir. Cuando el celular usa datos móviles, abre una sesión de datos con la red del operador (en LTE se llama conexión PDN), y el núcleo de esa red le asigna una IP. Con muchos operadores esa IP es privada: el celular no sale a internet con ella, sino a través de un CGNAT, un NAT del operador que reparte unas pocas IP públicas entre muchísimos usuarios</p>
<figure class="dg glass rv"><div class="tb"><i></i><i></i><i></i><em>cómo funciona</em></div><pre>celular
 <i>│</i> sesión de datos
 <i>▼</i>
torre <i>──►</i> núcleo del operador
            <i>│</i> asigna una IP privada (por ejemplo 10.x.x.x o 100.64.x.x)
            <i>▼</i>
          CGNAT
            <i>│</i> la traduce a una IP pública compartida
            <i>▼</i>
          lo que ve internet: <b>190.130.xxx.204</b></pre></figure>
<p class="rv">El rango <code>100.64.0.0/10</code> existe justamente para esto: el RFC 6598 lo reservó para el espacio entre el operador y su CGNAT. Y el RFC 6888 pide que, por defecto, un CGNAT mantenga la misma IP pública para una misma IP interna mientras dure su uso («paired pooling»), así las conexiones de un usuario no salen por direcciones distintas</p>
<p class="rv">Cuando el modo avión apaga el radio de verdad, esa sesión se cierra. Al volver, el celular abre una sesión nueva y el operador le vuelve a asignar una IP, que normalmente es otra. Pero lo que ve internet depende de qué dirección pública le toque en el CGNAT: puede ser otra, o puede repetirse la de antes</p>
<p class="rv">Por eso el resultado no es seguro. Hay al menos tres razones para que la IP salga igual:</p>
<ul class="rv"><li>El radio no se apagó de verdad. Si solo cambia un ajuste, la sesión sigue abierta y el operador no tiene por qué darte otra IP. Es justo lo que pasó en mi primer intento</li><li>El CGNAT te vuelve a dar la misma IP pública. La sesión es nueva, pero la dirección compartida que te toca puede repetirse</li><li>Lo que se mide no es la red móvil. Si el Wi-Fi sigue encendido, el tráfico sale por el internet de la casa, y esa IP no cambia por más veces que actives el modo avión</li></ul>
<p class="idea rv"><small>idea</small>En resumen: el modo avión no es lo que cambia la IP. Lo que la puede cambiar es que la sesión de datos se cierre y se abra otra; el modo avión es solo una forma de provocarlo, y no siempre lo logra</p>
<h2 class="rv" id="s3">El primer intento</h2>
<p class="rv">Los modos A y B del IP Rotator usaban esto:</p>
<figure class="cod glass rv" data-copiar="adb shell settings put global airplane_mode_on 1"><div class="tb"><i></i><i></i><i></i><em>terminal</em><button type="button" data-ok="copiado">copiar</button></div><pre><span class="pr">$ </span>adb shell settings put global airplane_mode_on 1</pre></figure>
<p class="rv">Parecía funcionar, porque el ajuste cambiaba. Pero los registros del radio (<code>logcat -b radio</code>) contaban otra historia: el radio nunca soltaba la torre. El comando solo cambiaba un valor guardado, así que el operador mantenía la misma sesión de CGNAT y la misma IP</p>
<p class="rv">En diciembre de 2025 la conclusión quedó escrita en el README de la v0.1.0: ADB no podía hacerlo sin permisos de sistema</p>
<h2 class="rv" id="s4">El comando correcto</h2>
<p class="rv">Seis meses después, mientras armaba Goket (mi panel de red), Claude encontró otro camino:</p>
<figure class="cod glass rv" data-copiar="adb shell cmd connectivity airplane-mode enable
adb shell cmd connectivity airplane-mode disable"><div class="tb"><i></i><i></i><i></i><em>terminal</em><button type="button" data-ok="copiado">copiar</button></div><pre><span class="pr">$ </span>adb shell cmd connectivity airplane-mode enable
<span class="pr">$ </span>adb shell cmd connectivity airplane-mode disable</pre></figure>
<p class="rv">La diferencia está en por dónde pasa. Este comando va por ConnectivityService, el mismo camino que usa el botón del sistema, y sí apaga el radio de verdad. Funciona sin root desde Android 12. En Android 16, en cambio, el aviso viejo de AIRPLANE_MODE ya ni siquiera se puede mandar desde ADB: lanza SecurityException</p>
<h2 class="rv" id="s5">Los detalles que hicieron falta</h2>
<p class="rv">El comando solo no alcanzaba. Hicieron falta tres cosas más:</p>
<ul class="rv"><li>El Wi-Fi del celular se apaga durante la prueba (<code>svc wifi disable</code>). Si no, el tethering sale por el internet de la casa y la IP que se mide no es la del operador</li><li>Con CGNAT no hay garantía de IP nueva al primer intento, así que se repiten ciclos cortos: 7 s apagado, 12 s para estabilizarse, hasta 5 veces</li><li>La IP se mide por la interfaz del tethering (<code>curl --interface</code>), así el PC no pierde su propio internet en ningún momento</li></ul>
<p class="rv">En la prueba real del 30 de septiembre de 2026 la IP cambió al primer intento, en 20 s. Salió como la v0.2.0</p>
<h2 class="rv" id="s6">Otras ideas, sin probar</h2>
<p class="rv">Si lo que cambia la IP es que la sesión de datos se cierre y se abra otra, el modo avión no debería ser el único camino. Dos ideas que quedan anotadas, todavía sin probar:</p>
<ul class="rv"><li><b>Apagar y prender solo los datos móviles.</b> Cerraría la sesión de datos sin apagar todo el radio, y quizás sería más rápido que un ciclo completo de modo avión</li><li><b>Cambiar el tipo de red preferida.</b> Pasar de 5G o 4G a 3G y volver obligaría al celular a registrarse de nuevo en la red, y con eso a abrir otra sesión</li></ul>
<p class="rv">Si alguna funciona, habría que comprobarla igual que el modo avión: medir la IP antes y después, y mirar los registros del radio</p>
<h2 class="rv" id="s7">Límites</h2>
<ul class="rv"><li>Lo medido sale de un solo celular y un solo operador. Otro operador puede asignar las IP de otra forma, o incluso dar una IP fija por línea</li><li>Cuántos ciclos hacen falta para una IP nueva depende del CGNAT del operador; los 5 intentos son un tope práctico, no un número medido en general</li><li>Las dos ideas de arriba son hipótesis: no hay datos todavía</li></ul>
<h2 class="rv" id="s8">Lo que me quedó</h2>
<p class="rv">La conclusión de diciembre era cierta para ese comando: con él, de verdad no se podía. Lo que estaba mal era dar la pregunta por cerrada cuando lo que había fallado era la herramienta. Y fueron los registros del radio, no la intuición, los que mostraron dónde estaba el problema</p>]]></content:encoded>
  </item>
  <item>
    <title>Decir «no sé» también es un resultado</title>
    <link>https://nuulm.com/es/notas/decir-no-se</link>
    <guid isPermaLink="true">https://nuulm.com/es/notas/decir-no-se</guid>
    <pubDate>Thu, 01 Oct 2026 12:00:00 +0000</pubDate>
    <description>Cómo inferir la torre celular que atiende a un celular sin GPS (Cell ID, RSRP, OpenCellID) con un nivel de confianza, y cómo clasificar la respuesta de un servidor de Minecraft con reglas que votan y admiten «ambiguo»</description>
    <category>diseño</category><category>incertidumbre</category><category>redes celulares</category>
    <enclosure url="https://nuulm.com/og/notas/decir-no-se.png" type="image/png" length="0"/>
    <content:encoded><![CDATA[<p class="rv">Hay herramientas que siempre te dan una respuesta, aunque no la tengan. En dos de mis proyectos tomé la decisión contraria: si los datos no alcanzan, el resultado lo dice</p>
<h2 class="rv" id="s1">Qué sabe un celular de la red</h2>
<p class="rv">Un celular conectado sabe más de lo que muestra: el país y el operador (MCC y MNC), el área (LAC en 3G, TAC en LTE y 5G), la celda que lo atiende (Cell ID) y la calidad de la señal. En LTE, la potencia de la señal de referencia se mide como RSRP, y su calidad como RSRQ (las dos están definidas en la especificación 3GPP TS 36.214); RSSNR es la relación señal/ruido. CellTraceLogger anota todo eso, en 5G NR, LTE y 3G</p>
<figure class="dg glass rv"><div class="tb"><i></i><i></i><i></i><em>cómo funciona</em></div><pre>observación (una línea NDJSON)
 <i>│</i> MCC · MNC · TAC · Cell ID · RSRP · RSRQ · RSSNR
 <i>▼</i>
celtrace <i>◄──</i> OpenCellID: Cell ID → posición aproximada
 <i>│</i> ¿una celda domina en el tiempo? ¿la señal es estable?
 <i>▼</i>
<b>torre más probable · confianza</b></pre></figure>
<p class="rv">Ojo con lo que identifica un Cell ID: la celda, que suele ser un sector de una antena, no un punto exacto. Y la posición de una celda en OpenCellID viene de mediciones colaborativas, así que también es aproximada. Por eso celltrace infiere infraestructura (qué torre o qué sector), no la ubicación de una persona</p>
<h2 class="rv" id="s2">celltrace: confianza baja, media o alta</h2>
<p class="rv">Cada inferencia trae un nivel de confianza según qué tan estable fue la señal y si una celda domina en el tiempo. Si hay celdas que se solapan o la red está inestable, lo dice en vez de elegir una. En la página del proyecto hay una simulación: cuando el celular queda entre dos torres, las observaciones se reparten y la confianza baja</p>
<h2 class="rv" id="s3">AccessScope: reglas que votan</h2>
<p class="rv">AccessScope entra a un servidor de Minecraft como lo haría un jugador (con Mineflayer) y clasifica lo que el servidor le responde: el mensaje de expulsión, el MOTD, la versión. La clasificación usa reglas escritas a mano, y las reglas votan. Si dos apuntan a cosas distintas, el resultado es «ambiguo» y no se elige ninguna:</p>
<figure class="cod glass rv" data-copiar="mensaje: &quot;This server is whitelisted. Banned players cannot join.&quot;

  whitelist  ✓  → lista blanca
  ban        ✓  → baneo
  ─────────────────────────────
  resultado     → ambiguo"><div class="tb"><i></i><i></i><i></i><em>terminal</em><button type="button" data-ok="copiado">copiar</button></div><pre>mensaje: "This server is whitelisted. Banned players cannot join."

  whitelist  ✓  → lista blanca
  ban        ✓  → baneo
  ─────────────────────────────
  resultado     → ambiguo</pre></figure>
<p class="rv">Tampoco se estira lo que dice la evidencia: un problema de cuenta no se presenta como baneo, porque no es evidencia de eso</p>
<h2 class="rv" id="s4">La idea de fondo: abstenerse</h2>
<p class="rv">Esto tiene nombre en reconocimiento de patrones: un clasificador con opción de rechazo. C. K. Chow lo formalizó en 1970: si un sistema puede negarse a decidir en los casos dudosos, comete menos errores en los que sí decide, a cambio de responder menos veces. Es un intercambio entre precisión y cobertura</p>
<figure class="dg glass rv"><div class="tb"><i></i><i></i><i></i><em>cómo funciona</em></div><pre>responder siempre      → cobertura alta, más errores dichos con seguridad
abstenerse si hay duda → cobertura menor, menos errores en lo que se responde
                         y los casos dudosos quedan <b>a la vista</b></pre></figure>
<p class="rv">Las dos herramientas eligen el segundo lado. Y en AccessScope la regla de agregación es conservadora a propósito: no gana la mayoría, hace falta que no haya contradicción</p>
<p class="idea rv"><small>idea</small>Un resultado equivocado dicho con seguridad hace más daño que un «no sé». Si la herramienta admite la duda, quien la usa sabe cuándo puede confiar y cuándo tiene que mirar más de cerca</p>
<h2 class="rv" id="s5">Límites</h2>
<ul class="rv"><li>Las reglas de AccessScope solo cubren los mensajes que ya vi; uno nuevo queda sin clasificar hasta que se escriba su regla. Así pasó con un mensaje de baneo en español que al principio no reconocía</li><li>La calidad de celltrace depende de cuántas celdas de la zona estén en OpenCellID; donde hay pocos datos, la confianza baja aunque la señal sea buena</li><li>No medí cuánto sube la precisión al abstenerse: el argumento es de diseño, no un resultado experimental</li></ul>]]></content:encoded>
  </item>
  <item>
    <title>Un mundo que vive en un solo lugar</title>
    <link>https://nuulm.com/es/notas/un-solo-lugar</link>
    <guid isPermaLink="true">https://nuulm.com/es/notas/un-solo-lugar</guid>
    <pubDate>Thu, 01 Oct 2026 12:00:00 +0000</pubDate>
    <description>Cómo un mod de Minecraft comparte un mundo entre amigos sin servidor prendido: un solo host a la vez, tokens de generación contra el split-brain, UPnP, STUN y relay para atravesar NAT y CGNAT, y el mundo que pasa de mano con JGit</description>
    <category>Minecraft</category><category>P2P</category><category>sistemas distribuidos</category>
    <enclosure url="https://nuulm.com/og/notas/un-solo-lugar.png" type="image/png" length="0"/>
    <content:encoded><![CDATA[<p class="rv">jukz es un mod de Minecraft para jugar con amigos en el mismo mundo sin dejar un servidor prendido. El mundo vive en el PC de quien esté jugando. La regla que ordena todo el diseño es simple: no hay copias que sincronizar, el mundo vive en un solo lugar a la vez</p>
<h2 class="rv" id="s1">El problema de fondo: dos líderes</h2>
<p class="rv">En sistemas distribuidos esto se conoce como split-brain: dos nodos creen al mismo tiempo que son el líder, y cada uno acepta cambios por su lado. Cuando se vuelven a encontrar ya no hay una versión correcta, hay dos. En un mundo de Minecraft eso significa construcciones o inventarios que se pierden</p>
<p class="rv">jukz lo evita de dos formas: preguntando antes de abrir, y resolviendo con un número de generación el caso en que dos personas abren a la vez</p>
<h2 class="rv" id="s2">Abrir un mundo es preguntar</h2>
<p class="rv">Cada mundo tiene un UUID permanente y un código corto para compartir. Al abrirlo, jukz pregunta si alguien ya lo tiene abierto: en la misma red, por multicast; desde casas distintas, en un servidor rendezvous propio escrito en Rust (Axum). Si alguien lo tiene, entras en vivo al mundo de esa persona. Si no, se abre en tu PC y quedas anunciado como host</p>
<p class="rv">Al principio había planeado una DHT para encontrarse, pero quedó descartada: el rendezvous era más simple y más confiable</p>
<h2 class="rv" id="s3">Nunca dos hosts</h2>
<p class="rv">Cada anuncio lleva un token con un número de generación. Si dos personas abren el mismo mundo a la vez, gana la generación más nueva y la otra se entera: puede quedarse con su copia local o entrar como invitada a la partida en vivo</p>
<figure class="cod glass rv" data-copiar="Nob abre el mundo      → host, generación 7
Kai lo abre a la vez   → host, generación 8
                         gana la 8
Nob se entera          → se queda con su copia local
                         o entra como invitado"><div class="tb"><i></i><i></i><i></i><em>terminal</em><button type="button" data-ok="copiado">copiar</button></div><pre>Nob abre el mundo      → host, generación 7
Kai lo abre a la vez   → host, generación 8
                         gana la 8
Nob se entera          → se queda con su copia local
                         o entra como invitado</pre></figure>
<p class="rv">La idea es la misma que Martin Kleppmann describe como fencing token: un número que solo crece y que acompaña a quien tiene el permiso. Lo que llega con un número viejo se reconoce como viejo, aunque su dueño todavía crea que manda</p>
<h2 class="rv" id="s4">Atravesar el NAT de cada casa</h2>
<p class="rv">Para que un amigo entre al mundo, su PC tiene que llegar al del host, y casi siempre hay un router con NAT en medio. jukz combina tres piezas:</p>
<figure class="dg glass rv"><div class="tb"><i></i><i></i><i></i><em>cómo funciona</em></div><pre>UPnP <i>──►</i> el host le pide a su router que abra un puerto
STUN <i>──►</i> cada PC averigua cuál es su dirección pública
 <i>│</i> si no se puede abrir el puerto (por ejemplo, detrás de CGNAT)
 <i>▼</i>
<b>relay por WebSocket</b> en el rendezvous: une las dos conexiones salientes</pre></figure>
<p class="rv">El último paso es la misma idea que estandariza TURN (RFC 8656): si dos equipos no pueden conectarse directo, los dos se conectan hacia afuera, a un tercero que pasa los datos. Cuesta un poco más de latencia, pero funciona aunque ninguno pueda recibir conexiones</p>
<h2 class="rv" id="s5">Pasar el mundo</h2>
<p class="rv">Cuando el host cierra con invitados conectados, guarda, toma una foto del mundo con JGit y se la pasa a un invitado, que la aplica y sigue como host. Si cierra sin nadie, la foto sube a la nube (R2) y el siguiente que abra el mundo la descarga y sigue desde ahí</p>
<p class="rv">Esto no salió bien a la primera. El 11 de junio el mundo se desincronizaba: al pasarlo al siguiente host se perdían cambios. La solución fue un relay por WebSocket en el rendezvous, por donde viaja la copia del mundo</p>
<h2 class="rv" id="s6">Probarlo sin abrir el juego</h2>
<p class="rv">La lógica difícil (elegir host, los tokens, el protocolo) vive en un módulo de Kotlin puro, sin Minecraft, con 67 pruebas que corren en segundos. El paso del mundo sí se probó dentro del juego, incluida una ida y vuelta A → B → A</p>
<p class="idea rv"><small>idea</small>Separar la lógica difícil del juego hizo posible probar los casos raros (dos aperturas a la vez, un host que se va) en segundos, sin abrir Minecraft cada vez</p>
<h2 class="rv" id="s7">Límites</h2>
<ul class="rv"><li>Está pensado para grupos chicos de amigos: un solo host a la vez significa que el mundo depende del PC y la conexión de quien hostea en ese momento</li><li>Las pruebas automáticas cubren la lógica; la red real se probó a mano, no con muchos jugadores ni en condiciones de red difíciles a escala</li><li>El relay agrega un salto más: es la opción de último recurso, no la ideal</li></ul>]]></content:encoded>
  </item>
</channel>
</rss>
