Starlink Mini 2 Behind CGNAT: Reaching a Site That Moves

Starlink Mini 2 Behind CGNAT: Reaching a Site That Moves

Starlink Mini 2 Behind CGNAT: Reaching a Site That Moves

Short answer (checked 2026-11-11): SpaceX has opened invitation-only preorders for the Starlink Mini 2, a portable dish with a removable battery. It will bring more cameras, sensors, field laptops and pop-up offices online where there is no fibre. What a standard plan will not give them is an address the internet can reach: Starlink's default IPv4 is carrier-grade NAT (CGNAT), and inbound connections are blocked. We counted the IPv4 space Starlink's network announces (990,208 addresses for more than 12 million customers), wrote a script that tells you whether a host is behind CGNAT, and compared four ways to reach a site that moves.

Sources. The Mini 2 as reported by Notebookcheck (Daniel Zlatev, 2026-11-10) and DishyTech (Colby Baber, 2026-11-09), discussed on Hacker News with 369 points and 380 comments by 2026-11-11. Address policy: Starlink Help Center articles "What IP address does Starlink provide?" and "IP Address", read 2026-11-11. Routing data: RIPEstat, queried by our script on 2026-11-11.

What is new in the Mini 2

Item Mini 2, as reported on 2026-11-10
Battery Removable 71 Wh, up to 4.5 h (SpaceX figure), USB-C charging at 60 W
Wi-Fi Wi-Fi 6 (the first Mini had Wi-Fi 5)
Build IP67, 314 x 263 x 23.1 mm, 1.6 kg with battery and kickstand
Availability Preorder by invitation for select US customers, more countries later
Price Not settled: $249 kit and $50 battery in Starlink's site code (DishyTech), a $999 figure elsewhere without a breakdown

No throughput figures have been published. For this article the battery is the point: a dish that runs four hours without mains is a dish that gets carried from site to site, and that changes how you reach whatever sits behind it.

The address problem: CGNAT by default

Starlink's help pages describe two IPv4 policies. The default one is CGNAT: the router gets an address from 100.64.0.0/10, the shared range of RFC 6598, many customers share each public address, and inbound IPv4 traffic is not allowed. A public IPv4 address is an option on Priority plans only (the two help pages name them differently: Local and Global Priority in one, Priority, Mobile Priority and Maritime in the other). Every line also gets a /56 IPv6 prefix through DHCPv6-PD, and Starlink says native IPv6 works on all its routers, kits and plans.

Two more lines matter for a portable dish. Starlink's own Wi-Fi routers support no port forwarding and no firewall rules, for IPv4 or for IPv6. And the reserved IPv4 address and IPv6 prefix survive a reboot, but may change when the dish is relocated or its software updates. A dish that moves is a dish whose address moves.

Why there is no address for everyone: our count

Our script asked RIPEstat what AS14593 (SPACEX-STARLINK) announces: 2,274 IPv4 prefixes that cover 990,208 unique addresses, most of them /23s and /24s. Starlink said in June 2026 that it had more than 12 million active customers (Mezha). That is about one public IPv4 address per 12 customers. At ip4.market's 90-day median for a /24, $25.25 per address (index updated 2026-11-02, see IPv4 prices), one address per customer would cost about $303 million; the space AS14593 announces is worth about $25 million at that rate. IPv6 is the opposite picture: 1,130 prefixes, 668 of them /40s, and one /40 holds 65,536 customer /56s, so those /40s alone hold about 43.8 million.

Limits of the count: RIPEstat leaves out routes seen by fewer than 10 RIS peers, Starlink may carry some traffic under other AS numbers, and announced space is not the same as space in use. The ratio is still the reason CGNAT is the default.

Script: is this host behind CGNAT?

cgnat_check.js lists the host's addresses and their kind (public, RFC 1918, 100.64.0.0/10), asks a public endpoint which address it sees, looks up the network that originates it and says whether inbound IPv4 can work. With an AS number as argument it also sums the address space that network announces, merging overlapping prefixes.

// 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, run on a DCXV Cloud server with Bun 1.4.4 on 2026-11-11 (host octets masked):

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

Our server holds its public address on eth0, so inbound connections reach it once the firewall allows them; IPv6 is simply not configured on this box. We have no Starlink line, so we show no Starlink output and make no claim about one. What the script does there: on a laptop behind the Starlink router the local address is private and the public one sits in an AS14593 prefix, so it reports NAT and prints the Starlink line; on a third-party router's WAN port it sees the 100.64.0.0/10 address directly and reports CGNAT.

Four ways to reach a site behind Starlink

Way Inbound from anyone? What it needs What breaks it
IPv6 straight to the site IPv6 visitors only Your own router (Starlink's has no firewall rules) The /56 can change on relocation; many networks are still IPv4-only
Priority plan, public IPv4 Yes A Priority plan The address can change on relocation or a software update
Mesh VPN (Tailscale, ZeroTier) Your own devices only An agent on every device Not for public visitors or devices that cannot run the agent
WireGuard relay on a cloud server Yes, fixed IPv4 A small server with a static IPv4 One more hop and one more machine to keep running

The relay works because CGNAT allows what it was built for: outbound connections. The site dials out to the relay, and PersistentKeepalive = 25 sends a packet every 25 seconds so the NAT mapping stays open (WireGuard's quick start gives 25 s as an interval that works with a wide range of firewalls). Visitors connect to the relay's address, which lives in a data centre and does not move with the dish.

The relay: two config files

Not run here: it needs root on two machines and a remote site. These are the standard wg-quick forms; 192.0.2.10 is a documentation address, put the relay's own in its 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

Three notes. MASQUERADE makes every connection arrive at the site from 10.99.0.1, so the site's logs lose the visitor's address; if you need it, drop that rule and route the site's replies back through wg0. Every packet now crosses the satellite link and the relay, so measure latency before you put a voice or control loop on it. And plan for the link itself going down: the July 2025 global Starlink outage lasted more than two hours.

Size and price of the relay

A relay forwards packets and does little else. Our Small cloud plan (2 vCPU, 4 GB RAM, 40 GB, EUR 15.00 a month, one static IPv4 included) is rated in our own task list for WireGuard with up to 25 tunnels; the VPN server page covers the same setup for teams. For fleets of devices that each need an address, see IoT and the IPv4 shortage.

Start a relay with us

Our Tier III data centres in Prague and Covilha run cloud servers, each with a static IPv4. Start one, run cgnat_check.js at the remote site and send the output to sales@dcxv.com: we will help you set up the relay.

Starlink Mini 2 Behind CGNAT: Reaching a Site That Moves
networkingcloudinfrastructure

Starlink Mini 2 Behind CGNAT: Reaching a Site That Moves

Starlink Mini 2 is out, but standard plans sit behind CGNAT. We counted 990,208 IPv4 addresses for 12M+ customers and compare four ways to reach a remote site.

LACNIC IPv4 Transfers in 2026: Rules, Fees, One-Year Hold
ipv4transfernetworking

LACNIC IPv4 Transfers in 2026: Rules, Fees, One-Year Hold

LACNIC IPv4 transfers in 2026: section 2.3.2.18 rules, the US$ 1,037 fee, NIC.br and NIC México, and a script over 1,929 logged transfers from 2016 to 2026.

Data Center Protests Reach Europe: What Buyers Should Do
clouddedicatedinfrastructure

Data Center Protests Reach Europe: What Buyers Should Do

Hundreds entered Microsoft's Amsterdam data center site on 6 November. The Dutch power numbers behind it, measured latency to EU hubs and what buyers should do.

US Voters Rejected Data Centers 45 to 1: What Buyers Learn
clouddedicatedinfrastructure

US Voters Rejected Data Centers 45 to 1: What Buyers Learn

On 3 November 47 US ballot measures on data centers went to voters and the restrictive side won 45. We recount the raw data: what it means for server buyers.

AWS BYOIP in 2026: $43.80 per IPv4 vs Your Own /24
ipv4pricingcloud

AWS BYOIP in 2026: $43.80 per IPv4 vs Your Own /24

AWS bills $0.005 an hour per public IPv4, $43.80 a year. BYOIP addresses carry no charge: when a bought or leased /24 pays back, with a ROA readiness script.