Starlink Mini 2 derrière CGNAT : joindre un site qui bouge

Starlink Mini 2 derrière CGNAT : joindre un site qui bouge

Starlink Mini 2 derrière CGNAT : joindre un site qui bouge

En bref (vérifié le 2026-11-11) : SpaceX a ouvert les précommandes sur invitation de la Starlink Mini 2, une antenne portable à batterie amovible. Elle mettra en ligne plus de caméras, de capteurs, de portables de terrain et de bureaux temporaires là où la fibre n'arrive pas. Ce qu'une offre standard ne leur donnera pas, c'est une adresse joignable depuis internet : l'IPv4 de Starlink passe par défaut par un NAT d'opérateur (CGNAT) et les connexions entrantes sont bloquées. Nous avons compté l'espace IPv4 annoncé par le réseau Starlink (990 208 adresses pour plus de 12 millions de clients), écrit un script qui dit si un hôte est derrière un CGNAT et comparé quatre façons de joindre un site qui se déplace.

Sources. La Mini 2 selon Notebookcheck (Daniel Zlatev, 2026-11-10) et DishyTech (Colby Baber, 2026-11-09), discutée sur Hacker News avec 369 points et 380 commentaires au 2026-11-11. Politique d'adressage : articles du Starlink Help Center "What IP address does Starlink provide?" et "IP Address", lus le 2026-11-11. Données de routage : RIPEstat, interrogé par notre script le 2026-11-11.

Ce qui change avec la Mini 2

Élément Mini 2, selon les articles du 2026-11-10
Batterie Amovible, 71 Wh, jusqu'à 4,5 h (chiffre de SpaceX), charge USB-C à 60 W
Wi-Fi Wi-Fi 6 (la première Mini avait le Wi-Fi 5)
Construction IP67, 314 x 263 x 23,1 mm, 1,6 kg avec batterie et béquille
Disponibilité Précommande sur invitation pour certains clients aux États-Unis, d'autres pays ensuite
Prix Non fixé : kit à 249 $ et batterie à 50 $ dans le code du site Starlink (DishyTech), un chiffre de 999 $ ailleurs, sans détail

Aucun débit n'a été publié. Pour cet article, c'est la batterie qui compte : une antenne qui tient quatre heures sans secteur est une antenne qu'on transporte d'un site à l'autre, et cela change la façon de joindre ce qui se trouve derrière.

Le problème d'adresse : CGNAT par défaut

Les pages d'aide de Starlink décrivent deux politiques IPv4. Celle par défaut est le CGNAT : le routeur reçoit une adresse de 100.64.0.0/10, la plage partagée du RFC 6598, de nombreux clients partagent chaque adresse publique et le trafic IPv4 entrant n'est pas autorisé. Une adresse IPv4 publique n'est proposée qu'en option sur les offres Priority (les deux pages d'aide les nomment différemment : Local et Global Priority dans l'une, Priority, Mobile Priority et Maritime dans l'autre). Chaque ligne reçoit aussi un préfixe IPv6 /56 par DHCPv6-PD, et Starlink indique que l'IPv6 natif fonctionne sur tous ses routeurs, kits et offres.

Deux phrases de plus comptent pour une antenne portable. Les routeurs Wi-Fi de Starlink ne gèrent ni la redirection de ports ni les règles de pare-feu, ni en IPv4 ni en IPv6. Et l'adresse IPv4 réservée et le préfixe IPv6 survivent à un redémarrage, mais peuvent changer quand l'antenne est déplacée ou que son logiciel est mis à jour. Une antenne qui bouge, c'est une adresse qui bouge.

Pourquoi il n'y a pas d'adresse pour tous : notre comptage

Notre script a demandé à RIPEstat ce qu'annonce l'AS14593 (SPACEX-STARLINK) : 2 274 préfixes IPv4 couvrant 990 208 adresses uniques, surtout des /23 et des /24. Starlink a annoncé en juin 2026 plus de 12 millions de clients actifs (Mezha). Cela fait environ une adresse IPv4 publique pour 12 clients. À la médiane sur 90 jours d'ip4.market pour un /24, 25,25 $ par adresse (indice mis à jour le 2026-11-02, voir prix IPv4), une adresse par client coûterait environ 303 millions de dollars ; l'espace annoncé par l'AS14593 vaut environ 25 millions à ce prix. En IPv6, c'est l'inverse : 1 130 préfixes, dont 668 en /40, et un /40 contient 65 536 /56 clients, donc ces seuls /40 en contiennent environ 43,8 millions.

Limites du comptage : RIPEstat écarte les routes vues par moins de 10 pairs RIS, Starlink peut faire passer une partie du trafic par d'autres numéros d'AS, et l'espace annoncé n'est pas l'espace utilisé. Ce rapport explique tout de même pourquoi le CGNAT est le choix par défaut.

Script : cet hôte est-il derrière un CGNAT ?

cgnat_check.js liste les adresses de l'hôte et leur type (publique, RFC 1918, 100.64.0.0/10), demande à un point public quelle adresse il voit, cherche le réseau qui l'annonce et dit si l'IPv4 entrant peut fonctionner. Avec un numéro d'AS en argument, il additionne aussi l'espace annoncé par ce réseau en fusionnant les préfixes qui se chevauchent.

// 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

Sortie, exécuté sur un serveur DCXV Cloud avec Bun 1.4.4 le 2026-11-11 (octets d'hôte masqués) :

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

Notre serveur porte son adresse publique sur eth0, donc les connexions entrantes l'atteignent dès que le pare-feu les autorise ; l'IPv6 n'est tout simplement pas configuré sur cette machine. Nous n'avons pas de ligne Starlink : nous ne montrons donc aucune sortie Starlink et n'affirmons rien à son sujet. Ce que fait le script là-bas : sur un portable derrière le routeur Starlink, l'adresse locale est privée et la publique se trouve dans un préfixe de l'AS14593, il signale donc un NAT et affiche la ligne Starlink ; sur le port WAN d'un routeur tiers, il voit directement l'adresse en 100.64.0.0/10 et signale un CGNAT.

Quatre façons de joindre un site derrière Starlink

Méthode Entrant pour tous ? Ce qu'il faut Ce qui la casse
IPv6 direct vers le site Visiteurs IPv6 seulement Votre propre routeur (celui de Starlink n'a pas de règles de pare-feu) Le /56 peut changer après un déplacement ; beaucoup de réseaux restent en IPv4 seul
Offre Priority, IPv4 publique Oui Une offre Priority L'adresse peut changer après un déplacement ou une mise à jour
VPN maillé (Tailscale, ZeroTier) Vos appareils seulement Un agent sur chaque appareil Inutilisable pour le public ou des appareils sans agent
Relais WireGuard sur un serveur cloud Oui, IPv4 fixe Un petit serveur avec IPv4 statique Un saut de plus et une machine de plus à faire tourner

Le relais fonctionne parce que le CGNAT autorise ce pour quoi il a été conçu : les connexions sortantes. Le site appelle le relais, et PersistentKeepalive = 25 envoie un paquet toutes les 25 secondes pour garder l'entrée NAT ouverte (le quick start de WireGuard donne 25 s comme intervalle compatible avec une grande variété de pare-feu). Les visiteurs se connectent à l'adresse du relais, qui vit dans un centre de données et ne bouge pas avec l'antenne.

Le relais : deux fichiers de configuration

Non exécuté ici : il faut root sur deux machines et un site distant. Ce sont les formes standard de wg-quick ; 192.0.2.10 est une adresse de documentation, mettez celle de votre relais à sa place.

# /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

Trois remarques. MASQUERADE fait arriver chaque connexion sur le site depuis 10.99.0.1, ses journaux perdent donc l'adresse du visiteur ; si vous en avez besoin, retirez cette règle et routez les réponses du site via wg0. Chaque paquet traverse désormais la liaison satellite et le relais : mesurez la latence avant d'y faire passer de la voix ou une boucle de contrôle. Et prévoyez la panne de la liaison elle-même : la panne mondiale de Starlink de juillet 2025 a duré plus de deux heures.

Taille et prix du relais

Un relais transmet des paquets et fait peu d'autre chose. Notre offre cloud Small (2 vCPU, 4 Go de RAM, 40 Go, 15,00 EUR par mois, une IPv4 statique incluse) est prévue dans notre propre liste de tâches pour WireGuard jusqu'à 25 tunnels ; la page serveur VPN couvre le même montage pour les équipes. Pour des parcs d'appareils qui ont chacun besoin d'une adresse, voir l'IoT et la pénurie d'IPv4.

Lancez un relais chez nous

Nos centres de données Tier III à Prague et Covilha hébergent des serveurs cloud, chacun avec une IPv4 statique. Lancez-en un, exécutez cgnat_check.js sur le site distant et envoyez la sortie à sales@dcxv.com : nous vous aiderons à monter le relais.

Starlink Mini 2 derrière CGNAT : joindre un site qui bouge
networkingcloudinfrastructure

Starlink Mini 2 derrière CGNAT : joindre un site qui bouge

La Starlink Mini 2 arrive, mais les offres standard sont derrière un CGNAT. 990 208 IPv4 pour 12 M+ de clients : nous comparons quatre accès à un site.

Transferts IPv4 LACNIC 2026 : règles, frais, un an de blocage
ipv4transfernetworking

Transferts IPv4 LACNIC 2026 : règles, frais, un an de blocage

Transferts IPv4 chez LACNIC en 2026 : règles de la section 2.3.2.18, frais de US$ 1,037, NIC.br et NIC México, et un script sur 1,929 transferts de 2016 à 2026.

Data centers : la contestation gagne l'Europe, que faire
clouddedicatedinfrastructure

Data centers : la contestation gagne l'Europe, que faire

Le 6 novembre, des centaines de personnes sont entrées sur le chantier Microsoft d'Amsterdam. Les chiffres du réseau, la latence mesurée et que faire.

Data centers rejetés 45 à 1 aux USA : leçons pour l'acheteur
clouddedicatedinfrastructure

Data centers rejetés 45 à 1 aux USA : leçons pour l'acheteur

47 scrutins locaux sur les data centers aux USA, le camp restrictif gagne 45 fois. Nous recomptons les données brutes : l'enjeu pour qui loue des serveurs.

AWS BYOIP en 2026 : $43.80 par IPv4 ou votre propre /24
ipv4pricingcloud

AWS BYOIP en 2026 : $43.80 par IPv4 ou votre propre /24

AWS facture $0.005 de l'heure par IPv4 publique, $43.80 par an. BYOIP échappe à ce coût : quand un /24 acheté ou loué est rentable, avec un script de ROA.