Starlink Mini 2 dietro CGNAT: raggiungere un sito che si sposta
In breve (verificato il 2026-11-11): SpaceX ha aperto i preordini su invito della Starlink Mini 2, un'antenna portatile con batteria rimovibile. Porterà in rete più telecamere, sensori, portatili da campo e uffici temporanei dove la fibra non arriva. Quello che un piano standard non darà loro è un indirizzo raggiungibile da internet: l'IPv4 di Starlink passa per impostazione predefinita da un NAT di operatore (CGNAT) e le connessioni in ingresso sono bloccate. Abbiamo contato lo spazio IPv4 annunciato dalla rete Starlink (990.208 indirizzi per oltre 12 milioni di clienti), scritto uno script che dice se un host è dietro CGNAT e confrontato quattro modi per raggiungere un sito che si sposta.
Fonti. La Mini 2 secondo Notebookcheck (Daniel Zlatev, 2026-11-10) e DishyTech (Colby Baber, 2026-11-09), discussa su Hacker News con 369 punti e 380 commenti al 2026-11-11. Politica degli indirizzi: articoli dello Starlink Help Center "What IP address does Starlink provide?" e "IP Address", letti il 2026-11-11. Dati di routing: RIPEstat, interrogato dal nostro script il 2026-11-11.
Cosa c'è di nuovo nella Mini 2
| Voce | Mini 2, secondo gli articoli del 2026-11-10 |
|---|---|
| Batteria | Rimovibile, 71 Wh, fino a 4,5 h (dato SpaceX), ricarica USB-C a 60 W |
| Wi-Fi | Wi-Fi 6 (la prima Mini aveva il Wi-Fi 5) |
| Struttura | IP67, 314 x 263 x 23,1 mm, 1,6 kg con batteria e supporto |
| Disponibilità | Preordine su invito per alcuni clienti negli USA, altri paesi in seguito |
| Prezzo | Non definito: kit a $249 e batteria a $50 nel codice del sito Starlink (DishyTech), altrove $999 senza dettaglio |
Non sono stati pubblicati dati di velocità. Per questo articolo conta la batteria: un'antenna che funziona quattro ore senza rete elettrica è un'antenna che si porta da un sito all'altro, e questo cambia il modo di raggiungere ciò che sta dietro.
Il problema degli indirizzi: CGNAT predefinito
Le pagine di aiuto di Starlink descrivono due politiche IPv4. Quella predefinita è il CGNAT: il router riceve un indirizzo da 100.64.0.0/10, l'intervallo condiviso dell'RFC 6598, molti clienti condividono ogni indirizzo pubblico e il traffico IPv4 in ingresso non è consentito. Un indirizzo IPv4 pubblico è un'opzione solo sui piani Priority (le due pagine di aiuto li chiamano in modo diverso: Local e Global Priority in una, Priority, Mobile Priority e Maritime nell'altra). Ogni linea riceve anche un prefisso IPv6 /56 tramite DHCPv6-PD, e Starlink dice che l'IPv6 nativo funziona su tutti i suoi router, kit e piani.
Per un'antenna portatile contano altre due frasi. I router Wi-Fi di Starlink non supportano né il port forwarding né regole di firewall, né in IPv4 né in IPv6. E l'indirizzo IPv4 riservato e il prefisso IPv6 sopravvivono a un riavvio, ma possono cambiare quando l'antenna viene spostata o il suo software si aggiorna. Un'antenna che si sposta è un indirizzo che si sposta.
Perché non c'è un indirizzo per tutti: il nostro conteggio
Il nostro script ha chiesto a RIPEstat cosa annuncia l'AS14593 (SPACEX-STARLINK): 2.274 prefissi IPv4 che coprono 990.208 indirizzi unici, per lo più /23 e /24. A giugno 2026 Starlink ha dichiarato oltre 12 milioni di clienti attivi (Mezha). Fa circa un indirizzo IPv4 pubblico ogni 12 clienti. Alla mediana a 90 giorni di ip4.market per un /24, $25,25 per indirizzo (indice aggiornato il 2026-11-02, vedi prezzi IPv4), un indirizzo per cliente costerebbe circa $303 milioni; lo spazio annunciato dall'AS14593 vale circa $25 milioni a quel prezzo. In IPv6 è il contrario: 1.130 prefissi, 668 dei quali /40, e un /40 contiene 65.536 /56 per clienti, quindi questi soli /40 ne contengono circa 43,8 milioni.
Limiti del conteggio: RIPEstat esclude le rotte viste da meno di 10 peer RIS, Starlink può far passare parte del traffico con altri numeri di AS, e lo spazio annunciato non è quello in uso. Il rapporto spiega comunque perché il CGNAT è l'impostazione predefinita.
Script: questo host è dietro CGNAT?
cgnat_check.js elenca gli indirizzi dell'host e il loro tipo (pubblico, RFC 1918, 100.64.0.0/10), chiede a un punto pubblico quale indirizzo vede, cerca la rete che lo annuncia e dice se l'IPv4 in ingresso può funzionare. Con un numero di AS come argomento somma anche lo spazio annunciato da quella rete, unendo i prefissi sovrapposti.
// 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
Output, eseguito su un server DCXV Cloud con Bun 1.4.4 il 2026-11-11 (ottetti dell'host mascherati):
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
Il nostro server ha l'indirizzo pubblico su eth0, quindi le connessioni in ingresso lo raggiungono appena il firewall le consente; l'IPv6 semplicemente non è configurato su questa macchina. Non abbiamo una linea Starlink, quindi non mostriamo output Starlink e non affermiamo nulla al riguardo. Cosa fa lì lo script: su un portatile dietro il router Starlink l'indirizzo locale è privato e quello pubblico sta in un prefisso dell'AS14593, quindi segnala NAT e stampa la riga Starlink; sulla porta WAN di un router di terze parti vede direttamente l'indirizzo in 100.64.0.0/10 e segnala CGNAT.
Quattro modi per raggiungere un sito dietro Starlink
| Modo | In ingresso da chiunque? | Cosa serve | Cosa lo rompe |
|---|---|---|---|
| IPv6 diretto al sito | Solo visitatori IPv6 | Un router proprio (quello di Starlink non ha regole di firewall) | Il /56 può cambiare dopo uno spostamento; molte reti sono ancora solo IPv4 |
| Piano Priority, IPv4 pubblico | Sì | Un piano Priority | L'indirizzo può cambiare dopo uno spostamento o un aggiornamento |
| VPN mesh (Tailscale, ZeroTier) | Solo i tuoi dispositivi | Un agente su ogni dispositivo | Non adatto a visitatori pubblici o dispositivi senza agente |
| Relay WireGuard su un server cloud | Sì, IPv4 fisso | Un piccolo server con IPv4 statico | Un hop in più e una macchina in più da gestire |
Il relay funziona perché il CGNAT consente ciò per cui è nato: le connessioni in uscita. Il sito chiama il relay, e PersistentKeepalive = 25 invia un pacchetto ogni 25 secondi perché la voce NAT resti aperta (il quick start di WireGuard indica 25 s come intervallo che funziona con un'ampia gamma di firewall). I visitatori si collegano all'indirizzo del relay, che vive in un data center e non si sposta con l'antenna.
Il relay: due file di configurazione
Non eseguito qui: servono root su due macchine e un sito remoto. Sono le forme standard di wg-quick; 192.0.2.10 è un indirizzo di documentazione, mettete al suo posto quello del vostro 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
Tre note. MASQUERADE fa arrivare ogni connessione al sito da 10.99.0.1, quindi i suoi log perdono l'indirizzo del visitatore; se serve, togliete quella regola e instradate le risposte del sito indietro via wg0. Ogni pacchetto attraversa ora il collegamento satellitare e il relay, quindi misurate la latenza prima di farci passare voce o un anello di controllo. E prevedete la caduta del collegamento stesso: il guasto globale di Starlink di luglio 2025 è durato più di due ore.
Dimensione e prezzo del relay
Un relay inoltra pacchetti e fa poco altro. Il nostro piano cloud Small (2 vCPU, 4 GB di RAM, 40 GB, EUR 15,00 al mese, un IPv4 statico incluso) è previsto nel nostro elenco di attività per WireGuard fino a 25 tunnel; la pagina server VPN copre la stessa configurazione per i team. Per flotte di dispositivi che hanno bisogno ciascuno di un indirizzo, vedi IoT e la scarsità di IPv4.
Avvia un relay con noi
I nostri data center Tier III a Praga e Covilha ospitano server cloud, ognuno con un IPv4 statico. Avviane uno, esegui cgnat_check.js sul sito remoto e invia l'output a sales@dcxv.com: ti aiutiamo a configurare il relay.
