Starlink Mini 2 за CGNAT: як дістатися до об'єкта, що переїжджає
Коротко (перевірено 2026-11-11): SpaceX відкрила передзамовлення Starlink Mini 2 за запрошеннями. Це портативна антена зі знімним акумулятором. Вона підключить до мережі більше камер, датчиків, польових ноутбуків і тимчасових офісів там, де немає оптики. Але стандартний тариф не дасть їм адреси, до якої можна підключитися з інтернету: IPv4 у Starlink за замовчуванням працює через операторський NAT (CGNAT), а вхідні з'єднання заблоковано. Ми порахували IPv4-простір, який анонсує мережа Starlink (990 208 адрес на понад 12 мільйонів клієнтів), написали скрипт, що показує, чи сидить хост за CGNAT, і порівняли чотири способи дістатися до об'єкта, який переїжджає.
Джерела. Mini 2 за даними Notebookcheck (Daniel Zlatev, 2026-11-10) і DishyTech (Colby Baber, 2026-11-09), обговорення на Hacker News: 369 балів і 380 коментарів на 2026-11-11. Політика адрес: статті Starlink Help Center "What IP address does Starlink provide?" і "IP Address", прочитано 2026-11-11. Дані маршрутизації: RIPEstat, запит нашим скриптом 2026-11-11.
Що нового в Mini 2
| Параметр | Mini 2, за повідомленнями від 2026-11-10 |
|---|---|
| Акумулятор | Знімний, 71 Вт-год, до 4,5 год (дані SpaceX), заряджання USB-C 60 Вт |
| Wi-Fi | Wi-Fi 6 (у першої Mini був Wi-Fi 5) |
| Корпус | IP67, 314 x 263 x 23,1 мм, 1,6 кг з акумулятором і підставкою |
| Доступність | Передзамовлення за запрошенням для частини клієнтів у США, інші країни пізніше |
| Ціна | Не визначена: комплект $249 і акумулятор $50 у коді сайту Starlink (DishyTech), в інших джерелах $999 без розбивки |
Даних про швидкість не опубліковано. Для цієї статті головне саме акумулятор: антену, яка працює чотири години без мережі живлення, носять з об'єкта на об'єкт, і це змінює те, як ви дістаєтеся до всього, що стоїть за нею.
Проблема адрес: CGNAT за замовчуванням
Довідка Starlink описує дві політики IPv4. Стандартна - CGNAT: роутер отримує адресу з 100.64.0.0/10, спільного діапазону з RFC 6598, багато клієнтів ділять одну публічну адресу, а вхідний IPv4-трафік заборонено. Публічна IPv4-адреса доступна лише як опція на тарифах Priority (дві сторінки довідки називають їх по-різному: Local і Global Priority в одній, Priority, Mobile Priority і Maritime в іншій). Кожна лінія також отримує IPv6-префікс /56 через DHCPv6-PD, і Starlink пише, що нативний IPv6 працює на всіх її роутерах, комплектах і тарифах.
Для портативної антени важливі ще два рядки. Власні Wi-Fi-роутери Starlink не підтримують ні проброс портів, ні правила файрвола, ні для IPv4, ні для IPv6. А зарезервовані IPv4-адреса й IPv6-префікс переживають перезавантаження, але можуть змінитися, коли антену переносять або оновлюється її програмне забезпечення. Антена, що переїжджає, - це адреса, що переїжджає.
Чому адрес не вистачає на всіх: наш підрахунок
Наш скрипт запитав у RIPEstat, що анонсує AS14593 (SPACEX-STARLINK): 2 274 IPv4-префікси, які покривають 990 208 унікальних адрес, переважно /23 і /24. У червні 2026 року Starlink повідомила про понад 12 мільйонів активних клієнтів (Mezha). Це приблизно одна публічна IPv4-адреса на 12 клієнтів. За 90-денною медіаною ip4.market для /24, $25,25 за адресу (індекс оновлено 2026-11-02, див. ціни на IPv4), одна адреса на клієнта коштувала б близько $303 мільйонів; простір, який анонсує AS14593, за цією ціною коштує близько $25 мільйонів. З IPv6 картина протилежна: 1 130 префіксів, з них 668 - /40, а один /40 вміщує 65 536 клієнтських /56, тож лише ці /40 вміщують близько 43,8 мільйона.
Межі підрахунку: RIPEstat не враховує маршрути, які бачать менше ніж 10 пірів RIS, Starlink може пропускати частину трафіку через інші AS, а анонсований простір - не те саме, що використаний. Але саме це співвідношення пояснює, чому CGNAT стоїть за замовчуванням.
Скрипт: чи цей хост за CGNAT?
cgnat_check.js показує адреси хоста та їхній тип (публічна, RFC 1918, 100.64.0.0/10), питає в публічної точки, яку адресу вона бачить, визначає мережу, що її анонсує, і каже, чи може працювати вхідний IPv4. З номером AS як аргументом він ще й підсумовує простір, який анонсує ця мережа, об'єднуючи префікси, що перекриваються.
// 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
Результат, запущено на хмарному сервері DCXV з Bun 1.4.4 2026-11-11 (октети хоста приховано):
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
Наш сервер тримає публічну адресу на eth0, тож вхідні з'єднання доходять до нього, щойно їх дозволить файрвол; IPv6 на цій машині просто не налаштовано. Лінії Starlink у нас немає, тому виводу зі Starlink ми не показуємо і нічого про нього не стверджуємо. Що робить скрипт там: на ноутбуці за роутером Starlink локальна адреса приватна, а публічна лежить у префіксі AS14593, тож він повідомляє про NAT і друкує рядок про Starlink; на WAN-порту стороннього роутера він бачить адресу з 100.64.0.0/10 напряму й повідомляє про CGNAT.
Чотири способи дістатися до об'єкта за Starlink
| Спосіб | Вхідні від будь-кого? | Що потрібно | Що ламає |
|---|---|---|---|
| IPv6 напряму до об'єкта | Лише IPv6-відвідувачі | Власний роутер (у Starlink немає правил файрвола) | /56 може змінитися після переїзду; багато мереж досі лише IPv4 |
| Тариф Priority, публічна IPv4 | Так | Тариф Priority | Адреса може змінитися після переїзду чи оновлення ПЗ |
| Mesh VPN (Tailscale, ZeroTier) | Лише ваші пристрої | Агент на кожному пристрої | Не для публічних відвідувачів і пристроїв без агента |
| Ретранслятор WireGuard на хмарному сервері | Так, фіксована IPv4 | Невеликий сервер зі статичною IPv4 | Ще один хоп і ще одна машина, яку треба підтримувати |
Ретранслятор працює, бо CGNAT дозволяє те, для чого його створено: вихідні з'єднання. Об'єкт сам підключається до ретранслятора, а PersistentKeepalive = 25 надсилає пакет кожні 25 секунд, щоб запис NAT лишався відкритим (у quick start WireGuard 25 с названо інтервалом, що працює з широким колом файрволів). Відвідувачі підключаються до адреси ретранслятора, яка живе в дата-центрі й не переїжджає разом з антеною.
Ретранслятор: два файли конфігурації
Тут не запускалося: потрібен root на двох машинах і віддалений об'єкт. Це стандартні форми wg-quick; 192.0.2.10 - документаційна адреса, підставте адресу свого ретранслятора.
# /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
Три примітки. MASQUERADE робить так, що кожне з'єднання приходить на об'єкт з 10.99.0.1, і журнали об'єкта втрачають адресу відвідувача; якщо вона потрібна, приберіть це правило й маршрутизуйте відповіді об'єкта назад через wg0. Кожен пакет тепер проходить і супутниковий канал, і ретранслятор, тож виміряйте затримку, перш ніж пускати туди голос чи контур керування. І плануйте на випадок падіння самого каналу: глобальний збій Starlink у липні 2025 року тривав понад дві години.
Розмір і ціна ретранслятора
Ретранслятор пересилає пакети й майже нічого більше не робить. Наш хмарний тариф Small (2 vCPU, 4 ГБ RAM, 40 ГБ, EUR 15,00 на місяць, одна статична IPv4 у ціні) у нашому ж переліку задач розрахований на WireGuard до 25 тунелів; сторінка VPN-сервер описує таке саме налаштування для команд. Для парку пристроїв, кожному з яких потрібна адреса, див. IoT і дефіцит IPv4.
Запустіть ретранслятор у нас
Наші дата-центри Tier III у Празі та Ковільяні мають хмарні сервери, кожен зі статичною IPv4. Запустіть сервер, виконайте cgnat_check.js на віддаленому об'єкті й надішліть результат на sales@dcxv.com: ми допоможемо налаштувати ретранслятор.
