Starlink Mini 2 hinter CGNAT: Standorte erreichen, die umziehen
Kurz gesagt (geprüft am 2026-11-11): SpaceX nimmt Vorbestellungen für die Starlink Mini 2 an, vorerst nur auf Einladung. Die tragbare Antenne hat einen abnehmbaren Akku. Sie wird mehr Kameras, Sensoren, Feld-Laptops und temporäre Büros dort ans Netz bringen, wo keine Glasfaser liegt. Was ein Standardtarif ihnen nicht gibt, ist eine Adresse, die aus dem Internet erreichbar ist: IPv4 läuft bei Starlink standardmäßig über Carrier-Grade NAT (CGNAT), eingehende Verbindungen sind gesperrt. Wir haben den IPv4-Adressraum gezählt, den das Starlink-Netz ankündigt (990.208 Adressen für mehr als 12 Millionen Kunden), ein Skript geschrieben, das zeigt, ob ein Host hinter CGNAT sitzt, und vier Wege verglichen, einen wandernden Standort zu erreichen.
Quellen. Die Mini 2 laut Notebookcheck (Daniel Zlatev, 2026-11-10) und DishyTech (Colby Baber, 2026-11-09), Diskussion auf Hacker News mit 369 Punkten und 380 Kommentaren bis 2026-11-11. Adressrichtlinie: Artikel des Starlink Help Center "What IP address does Starlink provide?" und "IP Address", gelesen am 2026-11-11. Routingdaten: RIPEstat, abgefragt von unserem Skript am 2026-11-11.
Was an der Mini 2 neu ist
| Merkmal | Mini 2, laut Berichten vom 2026-11-10 |
|---|---|
| Akku | Abnehmbar, 71 Wh, bis 4,5 h (Angabe von SpaceX), Laden per USB-C mit 60 W |
| WLAN | Wi-Fi 6 (die erste Mini hatte Wi-Fi 5) |
| Bauform | IP67, 314 x 263 x 23,1 mm, 1,6 kg mit Akku und Ständer |
| Verfügbarkeit | Vorbestellung auf Einladung für ausgewählte US-Kunden, weitere Länder später |
| Preis | Offen: Kit für $249 und Akku für $50 im Code der Starlink-Website (DishyTech), anderswo $999 ohne Aufschlüsselung |
Durchsatzwerte wurden nicht veröffentlicht. Für diesen Artikel zählt der Akku: Eine Antenne, die vier Stunden ohne Stromnetz läuft, wird von Standort zu Standort getragen, und das verändert, wie Sie erreichen, was dahinter hängt.
Das Adressproblem: CGNAT als Standard
Die Hilfeseiten von Starlink beschreiben zwei IPv4-Richtlinien. Standard ist CGNAT: Der Router bekommt eine Adresse aus 100.64.0.0/10, dem gemeinsam genutzten Bereich aus RFC 6598, viele Kunden teilen sich jede öffentliche Adresse, und eingehender IPv4-Verkehr ist nicht erlaubt. Eine öffentliche IPv4-Adresse gibt es nur als Option in Priority-Tarifen (die beiden Hilfeseiten benennen sie unterschiedlich: Local und Global Priority auf der einen, Priority, Mobile Priority und Maritime auf der anderen). Jeder Anschluss bekommt zudem ein IPv6-Präfix /56 per DHCPv6-PD, und laut Starlink funktioniert natives IPv6 auf allen Routern, Kits und Tarifen.
Für eine tragbare Antenne zählen zwei weitere Sätze. Die eigenen WLAN-Router von Starlink unterstützen weder Portweiterleitung noch Firewallregeln, weder für IPv4 noch für IPv6. Und die reservierte IPv4-Adresse und das IPv6-Präfix überstehen einen Neustart, können sich aber ändern, wenn die Antenne umgestellt wird oder ihre Software ein Update bekommt. Eine Antenne, die umzieht, ist eine Adresse, die umzieht.
Warum nicht jeder eine Adresse bekommt: unsere Zählung
Unser Skript fragte RIPEstat, was AS14593 (SPACEX-STARLINK) ankündigt: 2.274 IPv4-Präfixe mit 990.208 eindeutigen Adressen, überwiegend /23 und /24. Starlink meldete im Juni 2026 mehr als 12 Millionen aktive Kunden (Mezha). Das ist etwa eine öffentliche IPv4-Adresse pro 12 Kunden. Zum 90-Tage-Median von ip4.market für ein /24, $25,25 pro Adresse (Index aktualisiert am 2026-11-02, siehe IPv4-Preise), würde eine Adresse pro Kunde rund $303 Millionen kosten; der von AS14593 angekündigte Raum ist zu diesem Kurs rund $25 Millionen wert. Bei IPv6 ist es umgekehrt: 1.130 Präfixe, davon 668 /40, und ein /40 fasst 65.536 Kunden-/56, also fassen allein diese /40 rund 43,8 Millionen.
Grenzen der Zählung: RIPEstat lässt Routen weg, die weniger als 10 RIS-Peers sehen, Starlink kann Verkehr auch über andere AS-Nummern führen, und angekündigter Raum ist nicht genutzter Raum. Das Verhältnis erklärt trotzdem, warum CGNAT der Standard ist.
Skript: Sitzt dieser Host hinter CGNAT?
cgnat_check.js listet die Adressen des Hosts samt Art (öffentlich, RFC 1918, 100.64.0.0/10), fragt einen öffentlichen Endpunkt, welche Adresse er sieht, ermittelt das Netz, das sie ankündigt, und sagt, ob eingehendes IPv4 funktionieren kann. Mit einer AS-Nummer als Argument summiert es zusätzlich den Adressraum dieses Netzes und fasst überlappende Präfixe zusammen.
// 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
Ausgabe, ausgeführt auf einem DCXV Cloud-Server mit Bun 1.4.4 am 2026-11-11 (Host-Oktette maskiert):
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
Unser Server hält seine öffentliche Adresse auf eth0, eingehende Verbindungen erreichen ihn also, sobald die Firewall sie zulässt; IPv6 ist auf dieser Maschine schlicht nicht konfiguriert. Wir haben keinen Starlink-Anschluss, zeigen daher keine Starlink-Ausgabe und behaupten nichts darüber. Was das Skript dort tut: Auf einem Laptop hinter dem Starlink-Router ist die lokale Adresse privat und die öffentliche liegt in einem Präfix von AS14593, also meldet es NAT und druckt die Starlink-Zeile; am WAN-Port eines Fremdrouters sieht es die Adresse aus 100.64.0.0/10 direkt und meldet CGNAT.
Vier Wege zu einem Standort hinter Starlink
| Weg | Eingehend von allen? | Was nötig ist | Was ihn bricht |
|---|---|---|---|
| IPv6 direkt zum Standort | Nur IPv6-Besucher | Eigener Router (der von Starlink hat keine Firewallregeln) | Das /56 kann sich nach einem Umzug ändern; viele Netze sind noch reines IPv4 |
| Priority-Tarif, öffentliche IPv4 | Ja | Ein Priority-Tarif | Die Adresse kann sich nach Umzug oder Software-Update ändern |
| Mesh-VPN (Tailscale, ZeroTier) | Nur eigene Geräte | Ein Agent auf jedem Gerät | Nichts für öffentliche Besucher oder Geräte ohne Agent |
| WireGuard-Relay auf einem Cloud-Server | Ja, feste IPv4 | Ein kleiner Server mit statischer IPv4 | Ein Hop mehr und eine Maschine mehr, die laufen muss |
Das Relay funktioniert, weil CGNAT erlaubt, wofür es gebaut ist: ausgehende Verbindungen. Der Standort wählt sich beim Relay ein, und PersistentKeepalive = 25 schickt alle 25 Sekunden ein Paket, damit der NAT-Eintrag offen bleibt (der Quick Start von WireGuard nennt 25 s als Intervall, das mit sehr vielen Firewalls funktioniert). Besucher verbinden sich mit der Adresse des Relays, die im Rechenzentrum liegt und nicht mit der Antenne umzieht.
Das Relay: zwei Konfigurationsdateien
Hier nicht ausgeführt: Es braucht root auf zwei Maschinen und einen entfernten Standort. Das sind die üblichen wg-quick-Formen; 192.0.2.10 ist eine Dokumentationsadresse, setzen Sie die Ihres Relays ein.
# /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
Drei Hinweise. MASQUERADE lässt jede Verbindung beim Standort von 10.99.0.1 ankommen, die Logs dort verlieren also die Besucheradresse; brauchen Sie sie, entfernen Sie die Regel und leiten Sie die Antworten des Standorts über wg0 zurück. Jedes Paket läuft jetzt über die Satellitenstrecke und das Relay, messen Sie also die Latenz, bevor Sie Sprache oder einen Regelkreis darüber legen. Und planen Sie den Ausfall der Strecke selbst ein: der weltweite Starlink-Ausfall im Juli 2025 dauerte mehr als zwei Stunden.
Größe und Preis des Relays
Ein Relay leitet Pakete weiter und tut sonst wenig. Unser Cloud-Tarif Small (2 vCPU, 4 GB RAM, 40 GB, EUR 15,00 im Monat, eine statische IPv4 inklusive) ist in unserer eigenen Aufgabenliste für WireGuard mit bis zu 25 Tunneln ausgelegt; die Seite VPN-Server beschreibt denselben Aufbau für Teams. Für Geräteflotten, die jeweils eine Adresse brauchen, siehe IoT und die IPv4-Knappheit.
Starten Sie ein Relay bei uns
Unsere Tier-III-Rechenzentren in Prag und Covilha betreiben Cloud-Server, jeden mit statischer IPv4. Starten Sie einen, lassen Sie cgnat_check.js am entfernten Standort laufen und senden Sie die Ausgabe an sales@dcxv.com: Wir helfen beim Einrichten des Relays.
