Starlink Mini 2 atrás de CGNAT: como chegar a um local que muda

Starlink Mini 2 atrás de CGNAT: como chegar a um local que muda

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.

Starlink Mini 2 atrás de CGNAT: como chegar a um local que muda
networkingcloudinfrastructure

Starlink Mini 2 atrás de CGNAT: como chegar a um local que muda

A Starlink Mini 2 chegou, mas os planos normais ficam atrás de CGNAT. Contámos 990.208 IPv4 para 12+ milhões de clientes e comparamos quatro formas de acesso.

Transferências IPv4 na LACNIC em 2026: regras, taxas, um ano
ipv4transfernetworking

Transferências IPv4 na LACNIC em 2026: regras, taxas, um ano

Transferências IPv4 na LACNIC em 2026: regras da secção 2.3.2.18, taxa de US$ 1,037, NIC.br e NIC México, e um script sobre 1,929 transferências de 2016 a 2026.

Protestos contra centros de dados na Europa: o que fazer
clouddedicatedinfrastructure

Protestos contra centros de dados na Europa: o que fazer

A 6 de novembro, centenas entraram na obra da Microsoft em Amesterdão. Os números da rede neerlandesa, a latência medida a hubs da UE e o que fazer agora.

EUA rejeitam centros de dados 45 a 1: lições para clientes
clouddedicatedinfrastructure

EUA rejeitam centros de dados 45 a 1: lições para clientes

47 votações sobre centros de dados nos EUA, o lado restritivo venceu em 45. Recontamos os dados brutos e explicamos o que muda para quem aluga servidores.

AWS BYOIP em 2026: $43.80 por IPv4 ou o seu próprio /24
ipv4pricingcloud

AWS BYOIP em 2026: $43.80 por IPv4 ou o seu próprio /24

A AWS cobra $0.005 por hora por IPv4 público, $43.80 por ano. O BYOIP não paga isso: quando um /24 comprado ou alugado compensa, com um script de ROA.