NAT Traversal: как пробить корпоративный NAT для удалённого мониторинга

Проблема: 200 устройств за чужими NAT

Компания «МониторингПро» занимается удалённым мониторингом промышленного оборудования: датчики температуры, давления, вибрации на предприятиях клиентов. У них 200+ устройств (Raspberry Pi 4 и Orange Pi 5), установленных в сетях заказчиков.

Главная проблема: устройства находятся за NAT клиентских сетей, и «МониторингПро» не контролирует сетевую инфраструктуру. Получить белый IP или проброс порта — переговоры на недели, а у некоторых клиентов сеть за CG-NAT оператора.

Типы NAT, с которыми мы столкнулись:

  • Full Cone NAT (15% клиентов) — самый простой: любой внешний хост может отправить пакет на открытый порт. UDP hole punching работает тривиально.
  • Restricted Cone NAT (40%) — внешний хост должен совпадать с тем, куда отправлялся исходящий пакет. Hole punching работает при координации через сервер.
  • Port Restricted Cone NAT (30%) — проверяется и IP, и порт. Hole punching работает, но сложнее.
  • Symmetric NAT (15%) — для каждой пары (внутренний, внешний) создаётся уникальный mapping. Hole punching не работает. Нужен relay.

Задача: обеспечить стабильный SSH-доступ к каждому устройству и передачу телеметрии (MQTT) с минимальной задержкой, без требований к сетевой инфраструктуре клиента.

Решение 1: Reverse SSH — просто, но ненадёжно

Первое, что приходит в голову — обратный SSH-туннель. Устройство за NAT устанавливает SSH-соединение к серверу «МониторингПро» с белым IP и пробрасывает свой порт 22:

# На устройстве (Raspberry Pi за NAT):
ssh -N -R 10022:localhost:22 tunnel@relay.monitoringpro.ru

# На сервере relay.monitoringpro.ru:
ssh -p 10022 pi@localhost
# → попадаем на Raspberry Pi за NAT

Для автоматического восстановления соединения — autossh:

# /etc/systemd/system/reverse-tunnel.service
[Unit]
Description=Reverse SSH Tunnel
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=tunnel
ExecStart=/usr/bin/autossh -M 0 -N \
  -o "ServerAliveInterval 30" \
  -o "ServerAliveCountMax 3" \
  -o "ExitOnForwardFailure yes" \
  -R 10022:localhost:22 \
  -i /home/tunnel/.ssh/id_ed25519 \
  tunnel@relay.monitoringpro.ru
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

Проблемы этого подхода при 200+ устройствах:

  • Управление портами — каждому устройству нужен уникальный порт (10001-10200). Ручная таблица маппинга.
  • Безопасность — SSH-ключ на каждом устройстве. При компрометации одного устройства — доступ к relay-серверу.
  • Масштабируемость — 200 постоянных SSH-соединений к одному серверу — значительная нагрузка.
  • Обрывы — при нестабильном интернете autossh иногда «зависает» с мёртвым соединением.

Reverse SSH подходит для 5-10 устройств. Для 200 — нужно решение лучше.

Решение 2: WireGuard + централизованный hub

WireGuard — лёгкий VPN, который отлично работает за NAT благодаря UDP и встроенному keepalive:

# Сервер (hub) — relay.monitoringpro.ru
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.0.1/16
ListenPort = 51820
PrivateKey = 
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# Устройство 001
[Peer]
PublicKey = 
AllowedIPs = 10.10.1.1/32

# Устройство 002
[Peer]
PublicKey = 
AllowedIPs = 10.10.1.2/32

# ... и так 200 раз
# Устройство (Raspberry Pi за NAT)
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.10.1.1/32
PrivateKey = 

[Peer]
PublicKey = 
Endpoint = relay.monitoringpro.ru:51820
AllowedIPs = 10.10.0.0/16
PersistentKeepalive = 25   # Критично для NAT — не даёт mapping протухнуть
# Генерация ключей для нового устройства (скрипт)
#!/bin/bash
DEVICE_ID=$1
PRIVATE_KEY=$(wg genkey)
PUBLIC_KEY=$(echo $PRIVATE_KEY | wg pubkey)

echo "[Peer]" >> /etc/wireguard/wg0.conf
echo "PublicKey = $PUBLIC_KEY" >> /etc/wireguard/wg0.conf
echo "AllowedIPs = 10.10.1.${DEVICE_ID}/32" >> /etc/wireguard/wg0.conf

wg syncconf wg0 <(wg-quick strip wg0)  # Применяем без рестарта

echo "Device $DEVICE_ID configured. IP: 10.10.1.${DEVICE_ID}"

WireGuard решает проблемы SSH-туннелей: один UDP-порт для всех устройств, криптографическая идентификация вместо ключей SSH, минимальная нагрузка (~2% CPU на Raspberry Pi 4 при постоянном трафике). Но остаётся ручное управление конфигурацией при 200 устройствах.

Решение 3: Tailscale/Nebula — mesh VPN без сервера

Для «МониторингПро» мы в итоге выбрали Tailscale (использует WireGuard внутри) с собственным coordination-сервером Headscale:

# Установка Headscale на сервере (свой coordination server)
curl -fsSL https://github.com/juanfont/headscale/releases/download/v0.23.0/headscale_0.23.0_linux_amd64.deb \
  -o headscale.deb && sudo dpkg -i headscale.deb

# /etc/headscale/config.yaml
server_url: https://hs.monitoringpro.ru:443
listen_addr: 0.0.0.0:443
private_key_path: /var/lib/headscale/private.key
noise:
  private_key_path: /var/lib/headscale/noise_private.key
ip_prefixes:
  - 100.64.0.0/10
derp:
  server:
    enabled: true
    region_id: 900
    region_name: "MonitoringPro DC"
    stun_listen_addr: 0.0.0.0:3478

# Создаём пространство имён
headscale namespaces create devices
headscale namespaces create engineers
# На каждом устройстве — установка Tailscale
curl -fsSL https://tailscale.com/install.sh | sh

# Подключение к Headscale
tailscale up --login-server https://hs.monitoringpro.ru \
  --authkey $(headscale preauthkeys create --namespace devices --reusable --expiration 8760h)

# Проверяем подключение
tailscale status
# 100.64.0.1   relay-server      devices   linux   -
# 100.64.0.2   device-001        devices   linux   active; direct 192.168.1.50:41641
# 100.64.0.3   device-002        devices   linux   active; relay "MonitoringPro DC"

Преимущества Tailscale/Headscale для IoT:

  • Автоматический NAT traversal — Tailscale пробует UDP hole punching, и если не получается (symmetric NAT) — использует DERP relay. Из 200 устройств «МониторингПро» 170 установили прямое соединение, 30 работают через relay.
  • Zero-config mesh — каждое устройство видит каждое другое по стабильному IP (100.64.x.x). Не нужно настраивать маршруты.
  • ACL — инженеры видят все устройства, устройства не видят друг друга.
  • MagicDNSssh pi@device-042 вместо IP-адресов.

Альтернатива — Nebula (разработка Slack/Defined): полностью self-hosted, peer-to-peer mesh без relay. Требует certificate authority, но даёт полный контроль. Подходит для организаций, которые не хотят зависеть от внешних координационных серверов даже опционально.

ICMP-туннелирование: обход самых строгих фильтров

У нескольких клиентов «МониторингПро» сетевая политика блокирует весь исходящий трафик кроме ICMP (ping) и DNS. Такие среды встречаются на оборонных и энергетических предприятиях.

Концепция ICMP-туннеля: TCP/UDP-трафик инкапсулируется в ICMP Echo Request/Reply пакеты. NAT-устройства пропускают ICMP, используя поле ID как аналог порта для отслеживания сессий.

# Принцип работы ICMP-туннеля:
# 1. Клиент принимает TCP/UDP пакет
# 2. Извлекает payload
# 3. Оборачивает в ICMP Echo Request (type=8)
# 4. Отправляет на сервер через raw socket
# 5. Сервер распаковывает ICMP, извлекает payload
# 6. Пересылает в целевой сервис
# 7. Ответ идёт обратно через ICMP Echo Reply (type=0)

# Python — создание ICMP-пакета с произвольным payload
import socket
import struct

def create_icmp_packet(icmp_id, payload):
    """Создаёт ICMP Echo Request с данными внутри"""
    ICMP_ECHO_REQUEST = 8
    # Первый проход — checksum = 0
    header = struct.pack('!BBHHH',
        ICMP_ECHO_REQUEST, 0, 0, icmp_id, 1)
    # Считаем checksum
    csum = checksum(header + payload)
    # Второй проход — с реальным checksum
    header = struct.pack('!BBHHH',
        ICMP_ECHO_REQUEST, 0, csum, icmp_id, 1)
    return header + payload

def send_icmp(dest, icmp_id, data):
    sock = socket.socket(socket.AF_INET,
        socket.SOCK_RAW, socket.getprotobyname('icmp'))
    packet = create_icmp_packet(icmp_id, data)
    sock.sendto(packet, (dest, 0))
    sock.close()

На практике мы используем готовый инструмент hans (IP over ICMP):

# Сервер (с белым IP):
sudo hans -s 10.10.0.1 -p secretpass -d icmp0

# Клиент (за строгим NAT):
sudo hans -c relay.monitoringpro.ru -p secretpass -d icmp0

# Результат: TUN-интерфейс icmp0 с IP 10.10.0.100
# Весь трафик через этот интерфейс идёт внутри ICMP

# Далее поверх — WireGuard или SSH как обычно
ssh pi@10.10.0.100

Ограничения ICMP-туннеля:

  • Пропускная способность: 1-5 Мбит/с (пакеты маленькие, overhead высокий)
  • Латентность: +20-50 мс из-за инкапсуляции
  • MTU: ~1400 байт, нужна фрагментация
  • Некоторые DPI-системы определяют ICMP-туннели по аномально большому payload

Для телеметрии (MQTT-пакеты по 100-500 байт каждые 30 секунд) этих ограничений достаточно.

UDP Hole Punching: как это работает изнутри

UDP hole punching — основа всех современных P2P-протоколов (WebRTC, Tailscale, игровые сервера). Принцип:

  1. Устройства A и B оба за NAT. Ни одно не может принять входящее соединение.
  2. Оба подключаются к STUN-серверу (с белым IP), который сообщает каждому его внешний IP:port.
  3. A отправляет UDP-пакет на внешний IP:port B. NAT A создаёт mapping и пропускает исходящий пакет.
  4. Пакет A отбрасывается NAT B (B ещё не отправлял пакет A).
  5. B отправляет UDP-пакет на внешний IP:port A. NAT B создаёт mapping.
  6. Пакет B проходит через NAT A — потому что A уже создал mapping для этой пары.
  7. Двусторонняя связь установлена.
# Определение типа NAT с помощью STUN
# Установка: pip install pystun3
import stun

nat_type, external_ip, external_port = stun.get_ip_info(
    stun_host='stun.l.google.com',
    stun_port=19302
)
print(f"NAT Type: {nat_type}")
print(f"External: {external_ip}:{external_port}")

# Возможные результаты:
# - Full Cone → hole punching работает легко
# - Restricted Cone → hole punching работает с координацией
# - Port Restricted Cone → hole punching работает, но требует точных портов
# - Symmetric → hole punching НЕ работает, нужен TURN relay

Для случаев, когда hole punching невозможен (symmetric NAT), используется TURN-сервер как relay:

# Установка coturn (STUN/TURN сервер)
sudo apt install coturn

# /etc/turnserver.conf
listening-port=3478
tls-listening-port=5349
fingerprint
use-auth-secret
static-auth-secret=your-secret-key-here
realm=monitoringpro.ru
server-name=turn.monitoringpro.ru
total-quota=300
bps-capacity=0
no-multicast-peers

# Включаем автозапуск
sudo systemctl enable coturn

Итоговая архитектура и результаты

Для «МониторингПро» мы реализовали гибридную архитектуру:

  • Tailscale + Headscale — основной канал для 170 устройств (прямое P2P-соединение через hole punching)
  • DERP relay — для 25 устройств за symmetric NAT
  • ICMP-туннель (hans) — для 5 устройств на предприятиях с блокировкой всего кроме ICMP

Результаты после внедрения:

МетрикаДоПосле
Доступных устройств60% (белый IP/проброс)100%
Время подключения к устройству5-15 минут (звонок клиенту)3 секунды
Задержка телеметрии30+ минут (batch upload)< 1 секунды (realtime)
Время добавления устройства1-3 дня (проброс порта)5 минут
Стоимость (ежемесячно)VPN-шлюзы: 15 000 рубHeadscale VPS: 500 руб

Ключевой вывод: не существует одного универсального решения для NAT traversal. Tailscale/Nebula покрывают 85% случаев, WireGuard с ручным keepalive — ещё 10%, и для оставшихся 5% экстремальных случаев нужны ICMP-туннели или DNS-туннели.

Если у вас IoT-устройства за NAT или нужно организовать mesh-сеть между распределёнными площадками — обращайтесь на itfresh.ru. Мы подберём оптимальное решение под вашу сетевую реальность.

Часто задаваемые вопросы

WireGuard — однозначно. Он работает по UDP (OpenVPN тоже может, но часто используют TCP), имеет встроенный keepalive для NAT, потребляет в 5-10 раз меньше CPU (критично для ARM-устройств), и конфигурация в 10 строк вместо 100. OpenVPN оправдан только если нужен TCP-транспорт через HTTP-прокси.
Tailscale (с Headscale) — если важна простота и быстрый старт: установка в одну команду, MagicDNS, ACL из коробки. Nebula — если нужен полный контроль: своя PKI, нет зависимости от координационного сервера (lighthouse — опциональный), меньше потребление ресурсов. Для IoT-флота > 500 устройств рекомендуем Nebula.
Трафик внутри ICMP не шифруется по умолчанию — hans передаёт данные в открытом виде. Поэтому ICMP-туннель используется только как транспорт, а поверх него запускается WireGuard или SSH для шифрования. DPI-системы могут обнаружить ICMP-туннель по аномально большому payload (обычный ping — 64 байта, туннель — 1400). Используйте как крайнюю меру.
Используйте STUN-клиент: `pip install pystun3 && python -c "import stun; print(stun.get_ip_info())"`. Или утилиту nattype: `go install github.com/pion/stun/cmd/stun-nat-behaviour@latest`. Full Cone и Restricted Cone — hole punching сработает. Symmetric — нужен relay (TURN или DERP).
WireGuard масштабируется до 10 000+ peers на одном сервере (4 vCPU, 8 ГБ RAM) с агрегированной пропускной способностью ~10 Гбит/с. Каждый peer потребляет ~500 байт RAM. Узкое место — не WireGuard, а сетевая карта и канал. Для 200 IoT-устройств с телеметрией по 1 Кбит/с — запас в 1000 раз.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#nat traversal#stun#turn#udp hole punching#wireguard#tailscale#nebula#icmp tunnel
Комментарии 0

Оставить комментарий

загрузка...