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: мы поможем настроить ретранслятор.
