Starlink Mini 2 tras CGNAT: cómo llegar a un sitio que se mueve
Respuesta corta (comprobado el 2026-11-11): SpaceX ha abierto reservas por invitación de la Starlink Mini 2, una antena portátil con batería extraíble. Conectará más cámaras, sensores, portátiles de campo y oficinas temporales donde no llega la fibra. Lo que un plan estándar no les dará es una dirección accesible desde internet: el IPv4 de Starlink funciona por defecto con NAT de operador (CGNAT) y las conexiones entrantes están bloqueadas. Contamos el espacio IPv4 que anuncia la red de Starlink (990.208 direcciones para más de 12 millones de clientes), escribimos un script que indica si un host está tras CGNAT y comparamos cuatro formas de llegar a un sitio que se mueve.
Fuentes. La Mini 2 según Notebookcheck (Daniel Zlatev, 2026-11-10) y DishyTech (Colby Baber, 2026-11-09), debatida en Hacker News con 369 puntos y 380 comentarios a 2026-11-11. Política de direcciones: artículos del Starlink Help Center "What IP address does Starlink provide?" e "IP Address", leídos el 2026-11-11. Datos de enrutamiento: RIPEstat, consultado por nuestro script el 2026-11-11.
Qué trae de nuevo la Mini 2
| Elemento | Mini 2, según lo publicado el 2026-11-10 |
|---|---|
| Batería | Extraíble, 71 Wh, hasta 4,5 h (cifra de SpaceX), carga USB-C a 60 W |
| Wi-Fi | Wi-Fi 6 (la primera Mini tenía Wi-Fi 5) |
| Construcción | IP67, 314 x 263 x 23,1 mm, 1,6 kg con batería y soporte |
| Disponibilidad | Reserva por invitación para algunos clientes de EE. UU., más países después |
| Precio | Sin fijar: kit de $249 y batería de $50 en el código del sitio de Starlink (DishyTech), y una cifra de $999 en otras fuentes sin desglose |
No se han publicado cifras de velocidad. Para este artículo lo importante es la batería: una antena que funciona cuatro horas sin red eléctrica es una antena que se lleva de un sitio a otro, y eso cambia cómo se llega a lo que hay detrás.
El problema de las direcciones: CGNAT por defecto
Las páginas de ayuda de Starlink describen dos políticas IPv4. La predeterminada es CGNAT: el router recibe una dirección de 100.64.0.0/10, el rango compartido del RFC 6598, muchos clientes comparten cada dirección pública y no se permite tráfico IPv4 entrante. Una dirección IPv4 pública es una opción solo en los planes Priority (las dos páginas de ayuda los nombran de forma distinta: Local y Global Priority en una, Priority, Mobile Priority y Maritime en la otra). Cada línea recibe además un prefijo IPv6 /56 mediante DHCPv6-PD, y Starlink dice que el IPv6 nativo funciona en todos sus routers, kits y planes.
Para una antena portátil importan dos frases más. Los routers Wi-Fi propios de Starlink no admiten reenvío de puertos ni reglas de firewall, ni para IPv4 ni para IPv6. Y la dirección IPv4 reservada y el prefijo IPv6 sobreviven a un reinicio, pero pueden cambiar si la antena se traslada o se actualiza su software. Una antena que se mueve es una dirección que se mueve.
Por qué no hay dirección para todos: nuestro recuento
Nuestro script preguntó a RIPEstat qué anuncia AS14593 (SPACEX-STARLINK): 2.274 prefijos IPv4 que cubren 990.208 direcciones únicas, la mayoría /23 y /24. Starlink dijo en junio de 2026 que tenía más de 12 millones de clientes activos (Mezha). Eso es más o menos una dirección IPv4 pública por cada 12 clientes. A la mediana de 90 días de ip4.market para un /24, $25,25 por dirección (índice actualizado el 2026-11-02, ver precios IPv4), una dirección por cliente costaría unos $303 millones; el espacio que anuncia AS14593 vale unos $25 millones a ese precio. En IPv6 es al revés: 1.130 prefijos, 668 de ellos /40, y un /40 contiene 65.536 /56 de cliente, así que solo esos /40 contienen unos 43,8 millones.
Límites del recuento: RIPEstat excluye las rutas que ven menos de 10 peers de RIS, Starlink puede llevar parte del tráfico con otros números de AS, y el espacio anunciado no es el espacio en uso. Aun así, esa proporción explica por qué CGNAT es lo predeterminado.
Script: ¿está este host tras CGNAT?
cgnat_check.js lista las direcciones del host y su tipo (pública, RFC 1918, 100.64.0.0/10), pregunta a un punto público qué dirección ve, busca la red que la anuncia y dice si el IPv4 entrante puede funcionar. Con un número de AS como argumento, además suma el espacio que anuncia esa red, fusionando prefijos solapados.
// cgnat_check.js - am I behind CGNAT, and can anything reach me from outside?
// Usage: bun cgnat_check.js [ASN] (Bun or Node 18+, no dependencies)
import os from 'node:os'
const v4n = (ip) => ip.split('.').reduce((a, o) => a * 256 + Number(o), 0)
const inV4 = (ip, cidr) => {
const [net, bits] = cidr.split('/')
const size = 2 ** (32 - Number(bits))
return Math.floor(v4n(ip) / size) === Math.floor(v4n(net) / size)
}
const kindV4 = (ip) =>
inV4(ip, '100.64.0.0/10') ? 'CGNAT (RFC 6598)'
: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'].some((c) => inV4(ip, c)) ? 'private (RFC 1918)'
: inV4(ip, '169.254.0.0/16') ? 'link-local'
: inV4(ip, '127.0.0.0/8') ? 'loopback' : 'public'
const kindV6 = (ip) => {
const h = parseInt(ip.split(':')[0] || '0', 16)
return (h & 0xe000) === 0x2000 ? 'global' : (h & 0xfe00) === 0xfc00 ? 'ULA' : (h & 0xffc0) === 0xfe80 ? 'link-local' : 'other'
}
async function trace(url) {
try {
const r = await fetch(url, { signal: AbortSignal.timeout(5000) })
return Object.fromEntries((await r.text()).trim().split('\n').map((l) => l.split('=')))
} catch { return null }
}
async function ripestat(call, res) { // RIPEstat sometimes answers an HTML error page: retry
for (let i = 0; i < 3; i++) {
try {
const r = await fetch(`https://stat.ripe.net/data/${call}/data.json?resource=${res}&sourceapp=cgnat-check`, { signal: AbortSignal.timeout(15000) })
return (await r.json()).data
} catch { await new Promise((ok) => setTimeout(ok, 2000)) }
}
return null
}
async function origin(ip) {
const d = await ripestat('prefix-overview', ip)
return d ? { prefix: d.resource, asn: d.asns?.[0]?.asn, holder: d.asns?.[0]?.holder } : {}
}
const local = Object.entries(os.networkInterfaces()).flatMap(([dev, list]) =>
list.filter((a) => !a.internal).map((a) => ({ dev, ip: a.address, fam: a.family === 'IPv4' || a.family === 4 ? 4 : 6 })))
console.log('Local addresses:')
for (const a of local) console.log(` ${a.dev.padEnd(8)} ${a.ip.padEnd(40)} ${a.fam === 4 ? kindV4(a.ip) : kindV6(a.ip)}`)
const t4 = await trace('https://1.1.1.1/cdn-cgi/trace')
const t6 = await trace('https://[2606:4700:4700::1111]/cdn-cgi/trace')
const pub4 = t4?.ip, pub6 = t6?.ip
console.log(`\nSeen from outside: IPv4 ${pub4 ?? 'none'}, IPv6 ${pub6 ?? 'none'}`)
if (pub4) {
const o = await origin(pub4)
console.log(` ${pub4} in ${o.prefix ?? '?'} originated by AS${o.asn ?? '?'} ${o.holder ?? ''}`)
if (o.asn === 14593) console.log(' AS14593 = SpaceX Starlink')
}
const v4local = local.filter((a) => a.fam === 4)
let verdict
if (pub4 && v4local.some((a) => a.ip === pub4)) verdict = 'IPv4: public address on this host, inbound possible (firewall permitting)'
else if (v4local.some((a) => kindV4(a.ip).startsWith('CGNAT'))) verdict = 'IPv4: CGNAT on this host, no inbound IPv4'
else if (pub4) verdict = 'IPv4: behind NAT (home router and maybe CGNAT upstream): inbound only via port forward on every NAT layer'
else verdict = 'IPv4: no IPv4 path out'
console.log(`\n${verdict}`)
if (pub6 && local.some((a) => a.ip === pub6)) console.log('IPv6: global address on this host, inbound depends on the router firewall')
else console.log('IPv6: no global IPv6 path seen')
// Optional: bun cgnat_check.js AS14593 - how much address space does a network announce?
const asArg = process.argv[2]
if (asArg) {
const ps = (await ripestat('announced-prefixes', asArg)).prefixes.map((p) => p.prefix)
const v4 = ps.filter((p) => !p.includes(':')).map((p) => { const [n, b] = p.split('/'); return { s: v4n(n), e: v4n(n) + 2 ** (32 - b) } })
.sort((a, b) => a.s - b.s || b.e - a.e)
let total = 0, end = -1
for (const p of v4) { if (p.e > end) { total += p.e - Math.max(p.s, end); end = p.e } }
const v6 = ps.filter((p) => p.includes(':'))
const by = (l) => Object.entries(l.reduce((m, p) => ((m[p.split('/')[1]] = (m[p.split('/')[1]] || 0) + 1), m), {})).sort((a, b) => a[0] - b[0]).map(([k, n]) => `/${k} x${n}`).join(', ')
console.log(`\n${asArg}: ${v4.length} IPv4 prefixes, ${total.toLocaleString('en')} unique addresses; ${v6.length} IPv6 prefixes`)
console.log(` IPv4 sizes: ${by(ps.filter((p) => !p.includes(':')))}`)
console.log(` IPv6 sizes: ${by(v6)}`)
}
bun cgnat_check.js # this host only
bun cgnat_check.js AS14593 # plus the space a network announces
Salida, ejecutado en un servidor DCXV Cloud con Bun 1.4.4 el 2026-11-11 (octetos del host ocultos):
Local addresses:
eth0 178.23.189.x public
eth0 fe80::x link-local
int0 10.10.x.x private (RFC 1918)
int0 fe80::x link-local
Seen from outside: IPv4 178.23.189.x, IPv6 none
178.23.189.x in 178.23.189.0/24 originated by AS204057 DCXV-AS DCXV INTERNATIONAL LTD
IPv4: public address on this host, inbound possible (firewall permitting)
IPv6: no global IPv6 path seen
AS14593: 2274 IPv4 prefixes, 990,208 unique addresses; 1130 IPv6 prefixes
IPv4 sizes: /20 x2, /21 x28, /22 x58, /23 x1434, /24 x752
IPv6 sizes: /36 x1, /38 x67, /40 x668, /41 x173, /42 x11, /44 x4, /45 x4, /46 x11, /47 x20, /48 x171
Nuestro servidor tiene su dirección pública en eth0, así que las conexiones entrantes le llegan en cuanto el firewall las permite; el IPv6 simplemente no está configurado en esta máquina. No tenemos línea Starlink, así que no mostramos salida de Starlink ni afirmamos nada sobre ella. Lo que hace el script allí: en un portátil tras el router de Starlink la dirección local es privada y la pública está en un prefijo de AS14593, así que informa de NAT e imprime la línea de Starlink; en el puerto WAN de un router de terceros ve directamente la dirección de 100.64.0.0/10 e informa de CGNAT.
Cuatro formas de llegar a un sitio tras Starlink
| Forma | ¿Entrada desde cualquiera? | Qué necesita | Qué la rompe |
|---|---|---|---|
| IPv6 directo al sitio | Solo visitantes IPv6 | Router propio (el de Starlink no tiene reglas de firewall) | El /56 puede cambiar tras un traslado; muchas redes siguen siendo solo IPv4 |
| Plan Priority, IPv4 pública | Sí | Un plan Priority | La dirección puede cambiar tras un traslado o una actualización |
| VPN en malla (Tailscale, ZeroTier) | Solo sus dispositivos | Un agente en cada dispositivo | No sirve para visitantes públicos ni equipos sin agente |
| Relé WireGuard en un servidor cloud | Sí, IPv4 fija | Un servidor pequeño con IPv4 estática | Un salto más y una máquina más que mantener |
El relé funciona porque CGNAT permite aquello para lo que se creó: las conexiones salientes. El sitio llama al relé, y PersistentKeepalive = 25 envía un paquete cada 25 segundos para que la entrada NAT siga abierta (la guía rápida de WireGuard da 25 s como intervalo que funciona con una gran variedad de firewalls). Los visitantes se conectan a la dirección del relé, que vive en un centro de datos y no se mueve con la antena.
El relé: dos archivos de configuración
No ejecutado aquí: requiere root en dos máquinas y un sitio remoto. Son las formas estándar de wg-quick; 192.0.2.10 es una dirección de documentación, ponga en su lugar la de su relé.
# /etc/wireguard/wg0.conf on the cloud server (public IPv4 192.0.2.10)
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = <relay-private-key>
# forward TCP 443 that arrives on the public address to the site
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to-destination 10.99.0.2:443
PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
PostUp = iptables -A FORWARD -i eth0 -o wg0 -p tcp --dport 443 -j ACCEPT
PostUp = iptables -A FORWARD -i wg0 -o eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to-destination 10.99.0.2:443
PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE
PostDown = iptables -D FORWARD -i eth0 -o wg0 -p tcp --dport 443 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
[Peer]
# the site behind Starlink
PublicKey = <site-public-key>
AllowedIPs = 10.99.0.2/32
# /etc/wireguard/wg0.conf at the site (behind CGNAT)
[Interface]
Address = 10.99.0.2/24
PrivateKey = <site-private-key>
[Peer]
PublicKey = <relay-public-key>
Endpoint = 192.0.2.10:51820
AllowedIPs = 10.99.0.1/32
PersistentKeepalive = 25
wg genkey | tee private.key | wg pubkey > public.key
wg-quick up wg0
Tres notas. MASQUERADE hace que cada conexión llegue al sitio desde 10.99.0.1, así que sus registros pierden la dirección del visitante; si la necesita, quite esa regla y enrute las respuestas del sitio de vuelta por wg0. Cada paquete cruza ahora el enlace satelital y el relé, así que mida la latencia antes de poner voz o un lazo de control encima. Y prevea la caída del propio enlace: el corte global de Starlink de julio de 2025 duró más de dos horas.
Tamaño y precio del relé
Un relé reenvía paquetes y hace poco más. Nuestro plan cloud Small (2 vCPU, 4 GB de RAM, 40 GB, EUR 15,00 al mes, una IPv4 estática incluida) figura en nuestra propia lista de tareas para WireGuard con hasta 25 túneles; la página servidor VPN cubre el mismo montaje para equipos. Para flotas de dispositivos que necesitan una dirección cada uno, vea IoT y la escasez de IPv4.
Monte un relé con nosotros
Nuestros centros de datos Tier III en Praga y Covilha ofrecen servidores cloud, cada uno con una IPv4 estática. Inicie uno, ejecute cgnat_check.js en el sitio remoto y envíe la salida a sales@dcxv.com: le ayudaremos a montar el relé.
