Starlink Mini 2 atrás de CGNAT: como chegar a um local que muda
Resposta curta (verificado em 2026-11-11): a SpaceX abriu pré-encomendas por convite da Starlink Mini 2, uma antena portátil com bateria removível. Vai ligar à rede mais câmaras, sensores, portáteis de campo e escritórios temporários onde não há fibra. O que um plano normal não lhes dá é um endereço acessível a partir da internet: o IPv4 da Starlink funciona por omissão com NAT de operador (CGNAT) e as ligações de entrada estão bloqueadas. Contámos o espaço IPv4 que a rede da Starlink anuncia (990.208 endereços para mais de 12 milhões de clientes), escrevemos um script que diz se um host está atrás de CGNAT e comparámos quatro formas de chegar a um local que muda de sítio.
Fontes. A Mini 2 segundo a Notebookcheck (Daniel Zlatev, 2026-11-10) e a DishyTech (Colby Baber, 2026-11-09), discutida no Hacker News com 369 pontos e 380 comentários a 2026-11-11. Política de endereços: artigos do Starlink Help Center "What IP address does Starlink provide?" e "IP Address", lidos em 2026-11-11. Dados de encaminhamento: RIPEstat, consultado pelo nosso script em 2026-11-11.
O que há de novo na Mini 2
| Item | Mini 2, segundo as notícias de 2026-11-10 |
|---|---|
| Bateria | Removível, 71 Wh, até 4,5 h (valor da SpaceX), carga USB-C a 60 W |
| Wi-Fi | Wi-Fi 6 (a primeira Mini tinha Wi-Fi 5) |
| Construção | IP67, 314 x 263 x 23,1 mm, 1,6 kg com bateria e suporte |
| Disponibilidade | Pré-encomenda por convite para alguns clientes nos EUA, mais países depois |
| Preço | Por definir: kit de $249 e bateria de $50 no código do site da Starlink (DishyTech), um valor de $999 noutras fontes sem detalhe |
Não foram publicados valores de débito. Para este artigo o que conta é a bateria: uma antena que funciona quatro horas sem rede elétrica é uma antena levada de local em local, e isso muda a forma de chegar ao que está atrás dela.
O problema dos endereços: CGNAT por omissão
As páginas de ajuda da Starlink descrevem duas políticas IPv4. A predefinida é CGNAT: o router recebe um endereço de 100.64.0.0/10, a gama partilhada do RFC 6598, muitos clientes partilham cada endereço público e o tráfego IPv4 de entrada não é permitido. Um endereço IPv4 público é uma opção apenas nos planos Priority (as duas páginas de ajuda chamam-lhes nomes diferentes: Local e Global Priority numa, Priority, Mobile Priority e Maritime na outra). Cada linha recebe também um prefixo IPv6 /56 por DHCPv6-PD, e a Starlink diz que o IPv6 nativo funciona em todos os seus routers, kits e planos.
Para uma antena portátil contam mais duas frases. Os routers Wi-Fi da própria Starlink não suportam reencaminhamento de portas nem regras de firewall, nem em IPv4 nem em IPv6. E o endereço IPv4 reservado e o prefixo IPv6 sobrevivem a um reinício, mas podem mudar quando a antena é deslocada ou o software é atualizado. Uma antena que muda de sítio é um endereço que muda de sítio.
Porque não há endereço para todos: a nossa contagem
O nosso script perguntou ao RIPEstat o que anuncia o AS14593 (SPACEX-STARLINK): 2.274 prefixos IPv4 que cobrem 990.208 endereços únicos, na maioria /23 e /24. A Starlink disse em junho de 2026 que tinha mais de 12 milhões de clientes ativos (Mezha). Isso dá cerca de um endereço IPv4 público por cada 12 clientes. À mediana de 90 dias da ip4.market para um /24, $25,25 por endereço (índice atualizado em 2026-11-02, ver preços IPv4), um endereço por cliente custaria cerca de $303 milhões; o espaço que o AS14593 anuncia vale cerca de $25 milhões a esse preço. Em IPv6 é o contrário: 1.130 prefixos, 668 deles /40, e um /40 contém 65.536 /56 de cliente, pelo que só esses /40 contêm cerca de 43,8 milhões.
Limites da contagem: o RIPEstat deixa de fora rotas vistas por menos de 10 peers do RIS, a Starlink pode transportar parte do tráfego noutros números de AS, e espaço anunciado não é espaço em uso. Ainda assim, a proporção explica porque o CGNAT é a predefinição.
Script: este host está atrás de CGNAT?
cgnat_check.js lista os endereços do host e o seu tipo (público, RFC 1918, 100.64.0.0/10), pergunta a um ponto público que endereço vê, procura a rede que o anuncia e diz se o IPv4 de entrada pode funcionar. Com um número de AS como argumento, soma também o espaço anunciado por essa rede, juntando prefixos sobrepostos.
// 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
Saída, executado num servidor DCXV Cloud com Bun 1.4.4 em 2026-11-11 (octetos do host ocultados):
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
O nosso servidor tem o endereço público em eth0, por isso as ligações de entrada chegam-lhe assim que a firewall as permita; o IPv6 simplesmente não está configurado nesta máquina. Não temos linha Starlink, por isso não mostramos saída da Starlink nem afirmamos nada sobre ela. O que o script faz lá: num portátil atrás do router da Starlink o endereço local é privado e o público está num prefixo do AS14593, por isso indica NAT e imprime a linha da Starlink; na porta WAN de um router de terceiros vê diretamente o endereço de 100.64.0.0/10 e indica CGNAT.
Quatro formas de chegar a um local atrás da Starlink
| Forma | Entrada de qualquer um? | O que precisa | O que a quebra |
|---|---|---|---|
| IPv6 direto ao local | Só visitantes IPv6 | Router próprio (o da Starlink não tem regras de firewall) | O /56 pode mudar após uma deslocação; muitas redes ainda são só IPv4 |
| Plano Priority, IPv4 público | Sim | Um plano Priority | O endereço pode mudar após uma deslocação ou atualização |
| VPN em malha (Tailscale, ZeroTier) | Só os seus dispositivos | Um agente em cada dispositivo | Não serve para visitantes públicos nem equipamentos sem agente |
| Relay WireGuard num servidor cloud | Sim, IPv4 fixo | Um servidor pequeno com IPv4 estático | Mais um salto e mais uma máquina para manter |
O relay funciona porque o CGNAT permite aquilo para que foi feito: ligações de saída. O local liga-se ao relay, e PersistentKeepalive = 25 envia um pacote a cada 25 segundos para manter aberta a entrada NAT (o quick start do WireGuard indica 25 s como intervalo que funciona com uma grande variedade de firewalls). Os visitantes ligam-se ao endereço do relay, que vive num centro de dados e não muda de sítio com a antena.
O relay: dois ficheiros de configuração
Não executado aqui: precisa de root em duas máquinas e de um local remoto. São as formas padrão do wg-quick; 192.0.2.10 é um endereço de documentação, use o do seu relay.
# /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
Três notas. MASQUERADE faz com que cada ligação chegue ao local a partir de 10.99.0.1, e os registos do local perdem o endereço do visitante; se precisar dele, retire essa regra e encaminhe as respostas do local de volta por wg0. Cada pacote passa agora pela ligação por satélite e pelo relay, por isso meça a latência antes de pôr voz ou um ciclo de controlo por cima. E planeie a falha da própria ligação: a falha global da Starlink em julho de 2025 durou mais de duas horas.
Dimensão e preço do relay
Um relay reencaminha pacotes e pouco mais. O nosso plano cloud Small (2 vCPU, 4 GB de RAM, 40 GB, EUR 15,00 por mês, um IPv4 estático incluído) está previsto na nossa própria lista de tarefas para WireGuard com até 25 túneis; a página servidor VPN cobre a mesma montagem para equipas. Para frotas de dispositivos que precisam de um endereço cada, veja IoT e a escassez de IPv4.
Monte um relay connosco
Os nossos centros de dados Tier III em Praga e na Covilhã têm servidores cloud, cada um com um IPv4 estático. Inicie um, corra cgnat_check.js no local remoto e envie a saída para sales@dcxv.com: ajudamos a montar o relay.
