Skip to content

Traffic Fly: вход из интернета через VPS ​

Сервис стоит дома или в офисе за NAT, а пользователям нужен адрес в интернете. Traffic Fly принимает трафик на дешёвом VPS с белым адресом и отдаёт его по туннелю WireGuard на хосты — в контейнер, в виртуальную машину, в LXD или в службу самого хоста. Клиент виден целям под своим настоящим IP, работают TCP и UDP, а на одном порту можно держать пул целей с балансировкой и проверками.

Настраивает всё хаб — в разделе меню «Traffic Fly», но работают шлюз и хосты без хаба: туннели, правила и маршрутизатор поднимаются сами после перезагрузки.

Traffic Fly: карта и маршруты

Схема ​

клиенты ──80/443 по имени, TCP/UDP-порты──▶ VPS: nkt-edge (роль fly)
                                               │  WireGuard 51820/udp
                                               ▼
                                хост: туннель nkt-flyN → DNAT → контейнер,
                                машина, LXD, служба хоста, адрес в его сети
  • Шлюз — VPS с nkt-edge в роли fly. На шлюзе работает отдельная служба nkt-fly (root): WireGuard-интерфейс nkt-fly, правила nftables и маршрутизатор по именам. Сам edge остаётся без прав и только передаёт ей команды хаба по туннелю.
  • Хост — у каждого хоста по туннелю к каждому шлюзу (nkt-flyN, wg-quick@nkt-flyN). Закрытые ключи создаются на своей машине и никуда не уходят — хаб видит только публичные.
  • Ответы целей уходят обратно в тот же туннель по метке соединения — маршрут хоста по умолчанию не меняется.

Маршруты ​

ВидЧтоIP клиента
HTTPS по именипорт 443, выбор по SNI; TLS не расшифровывается — сертификат у целиPROXY protocol v2
HTTPS по имени, сертификат на шлюзепорт 443; TLS завершает шлюз, цели — обычный HTTP (см. ниже)X-Forwarded-For, X-Real-IP
HTTP по именипорт 80, выбор по заголовку HostPROXY protocol v2
TCP-порт, UDP-порт, TCP+UDPпорт целиком — в пулнастоящий, без доработок цели
  • 80 и 443 делятся по именам: app.example.com, *.example.com; точное имя важнее маски. Одно имя — один маршрут.
  • Диапазон портов (поле «до порта», TCP/UDP): порт сохраняется насквозь, работает одна цель — первая живая; для relay-портов coturn и подобного (см. пример).
  • Остальные порты: один порт шлюза — один пул. Нужен второй пул на том же порту — возьмите другой порт или второй шлюз (VPS с другим адресом).
  • Маршрут по имени передаёт IP клиента PROXY protocol v2: цель должна его принимать (nginx listen 443 proxy_protocol, Caddy, HAProxy, Traefik). Без галочки цель увидит адрес шлюза в туннеле.
  • Маршрут по порту идёт через ядро (DNAT без подмены источника): цель видит настоящий адрес клиента сама.

DNS через плагин ​

Подключён DNS-плагин — у маршрута по имени появляется галочка «DNS через плагин»: при «Применить» хаб сам ставит A-записи имён маршрута на адрес шлюза. Одно имя в маршрутах с разными шлюзами — запись Failover: DNS отдаёт только живые шлюзы. В сценарии — fly route … dns plugin, в конвейере — dns: plugin в блоке fly:. Сухой прогон «Применить» показывает, какие записи изменятся.

Сертификат на шлюзе ​

Если цель отдаёт обычный HTTP (контейнер с приложением без своего сертификата), у маршрута «HTTPS по имени» отметьте «Сертификат на шлюзе». Без неё шлюз TLS не расшифровывает, и браузер покажет ERR_SSL_PROTOCOL_ERROR.

  • Шлюз сам получает и продлевает сертификат Let's Encrypt на имена маршрута. Проверка — по его порту 80, поэтому DNS имён должен указывать на шлюз, а порт 80 шлюза — быть свободным или под маршрутами «HTTP по имени». Нужны точные имена: *.домен так не выпустить.
  • Порты принадлежат шлюзу, не хосту: на хосте открывать ничего не нужно. ufw самого VPS nkt настраивает при установке edge, а в межсетевом экране провайдера (security group, «файрвол» в панели хостинга) разрешите 80 и 443 и порты маршрутов по порту (например, 2222 для git по SSH) — туда nkt не дотягивается.
  • http://имя перенаправляется на https://.
  • Цели уходит HTTP с адресом клиента в X-Forwarded-For и X-Real-IP, схема — в X-Forwarded-Proto: https; заголовок Host — как у клиента. PROXY protocol не нужен.
  • Сертификаты и ключи — только на шлюзе (/etc/nkt-edge/fly/certs, только root), с хаба не передаются; продление идёт и без хаба.
  • Окно маршрута показывает по каждому имени: «сертификат до …» или почему он не выпущен.
  • «Сухой прогон» и «Применить» проверяют заранее: шлюз не старше 1.13.25 (иначе отказ — «Переустановить» у шлюза), DNS имени указывает на шлюз и порт 80 шлюза отвечает снаружи (иначе предупреждение в журнале задания: хаб может и не выходить в интернет). Версия шлюза видна в «Шлюзах».

Доступ только с адресов ​

У маршрута можно задать разрешённые адреса клиентов — IPv4-адреса и сети (203.0.113.7, 198.51.100.0/24, до 200 штук). Остальных отсекает сам шлюз: у маршрутов по порту — ядро (их пакеты не уходят на цели), у маршрутов по имени — маршрутизатор, ещё до цели. Сколько соединений отсечено, видно в окне маршрута. Проверки Let's Encrypt для сертификата на шлюзе проходят всегда.

Так стоит открывать то, что не должно быть видно всему интернету: консоли машин, служебные порты, админки. В конвейере — поле allow: у блока fly:.

Ограничения на клиента ​

У маршрута — два необязательных поля:

  • соединений с одного адреса, не больше — одновременных;
  • новых в секунду с одного адреса, не больше — частота новых соединений (с запасом на всплеск в то же число).

Лишние соединения шлюз отбрасывает сам: у маршрутов по порту — ядро (до DNAT, цель их не видит), у маршрутов по имени — маршрутизатор. Сколько отсечено, видно в окне маршрута. Пусто — без ограничений. Меняются в редакторе маршрута в любой момент, в конвейере — max_conns: и rate: у блока fly:.

Производительность и безопасность ​

Производительность. Замеры на машине разработки (16 ядер, loopback и veth в пространствах имён), не на VPS — это верхняя граница самого Traffic Fly, а не вашего канала:

ПутьРезультат
маршрут по порту (ядро: DNAT шлюза → туннель → DNAT хоста)~17 Гбит/с в один поток, ~50 Гбит/с в четыре
маршрут по имени без расшифровки (маршрутизатор копирует байты)~23 Гбит/с, ~9 000 новых соединений/с на 4 ядрах
маршрут по имени с сертификатом на шлюзе~7,9 Гбит/с, ~2 300 новых соединений/с на 4 ядрах (вместе с клиентом)

На VPS упрётся раньше в шифрование WireGuard (ориентир — от сотен Мбит/с на 1 vCPU до гигабита и больше на ядро) и в сеть провайдера. Задержка — плюс путь «шлюз ↔ хост». Замеры воспроизводятся: go test ./internal/fly -run x -bench Router и NKT_FLY_BENCH=1 go test ./internal/fly -run DataPathThroughput -v.

Безопасность.

  • Закрытые ключи WireGuard и сертификатов не покидают своих машин; хаб видит только открытые. nkt-edge работает без прав, nkt-fly — root (ему нужна сеть), команды принимает только на loopback и только с токеном.
  • Маршрутизатор написан на Go, читает начало соединения не больше 16 КБ и со сроком 10 секунд.
  • Хост пускает из туннеля только то, что пришло на служебные порты маршрутов, ответы и ICMP: взломанный шлюз не достучится до остальных служб хоста и сетей контейнеров.
  • Белый список адресов и ограничения на клиента — у каждого маршрута.
  • Размер TCP-сегментов в туннеле выравнивается по его MTU (MSS) — крупные пакеты не застревают, даже если по дороге режут ICMP.
  • С сертификатом на шлюзе TLS расшифровывает шлюз: взломанный шлюз видит этот трафик. Для чувствительного — оставляйте сертификат у цели.
  • Внешнего аудита Traffic Fly не проходил.

Пулы и балансировка ​

В маршруте — одна или несколько целей: хост и что на нём (контейнер, машина, LXD-инстанс, сам хост или адрес в сети хоста) и порт. Адрес контейнера или машины хаб узнаёт при применении.

Маршрут: имена, балансировка, цели

БалансировкаГде
меньше соединениймаршруты по имени
по кругувезде
по IP клиента (липко)везде; клиент держится одной цели
  • Вес — доля трафика цели относительно остальных.
  • Вывод (drain) — новых соединений цель не получает, текущие доживают; так цель выводят на обслуживание.
  • Проверка: TCP-подключение, HTTP-запрос (путь и код ответа) или без проверки; можно указать отдельный порт проверки. Цель считается недоступной после 3 неудач подряд и снова живой после 2 удач; проверка — раз в 5 с. Недоступная цель убирается из пула сразу, в том числе для маршрутов по порту. UDP-маршруту нужен TCP-порт проверки или «без проверки».

Карта и сводка ​

Вверху вкладки — карта: шлюзы → маршруты → цели; хост пишется перед именем цели (web-1 › acme-app:8443), длинные имена сокращаются по середине, полное — в подсказке. Состояние туннеля к хосту — цветом рамки цели и подписью («UDP блокируется», «не настроен»). Цвет — состояние (зелёный — все живы, жёлтый — часть, красный — никого), толщина связи — число активных соединений; карта обновляется раз в 5 с. Щелчок по маршруту — подробности: цели с проверкой, откликом и соединениями, трафик и число клиентов. Администратор видит адреса клиентов, пока их не больше 30; дальше — только число. Наблюдатели видят карту и число клиентов, адресов — нет.

Правка на карте ​

Кнопка «Правка карты» (администратору) включает правку: справа, четвёртым столбцом, — панель «Контейнеры и машины» по хостам, с поиском; кандидаты нарисованы такими же блоками, как цели (зелёная рамка — работает, серая — остановлен; ниже — адрес и порты). Рядом с именем хоста — число целей этого вида; хосты без них и без связи скрыты («скрыто хостов без целей: N — показать»). Под картой — корзина.

Всё делается мышью (или пальцем на телефоне):

  • Контейнер из панели на маршрут или любую его цель — встаёт в пул резервом: проверяется, но трафика не получает, пока на него не переключат. Порт — объявленный контейнером или как у первой цели маршрута; неизвестен — карта спросит.
  • Цель тащится на другой маршрут — переносится туда резервом; в корзину — убирается из пула.
  • Обмен: у маршрута рабочие цели нарисованы сверху, резервные — под ними. Резервную тащат на рабочую (или рабочую на резервную) того же маршрута — эти две меняются местами: резервная встаёт в работу, рабочая уходит в резерв, остальные цели пула не трогаются. Пока тащите, место подсвечено зелёным «↑ переключить сюда» или красным «не жива»: в работу встаёт только цель, живая по проверке, и только после того же вопроса «Переключить трафик с A на B?».
  • Колесо мыши над целью — вес ±1.
  • Щелчок по цели — меню: «Переключить трафик сюда», «В работу», «В резерв», «Изменить» (порт, вес), «Перенести в другой маршрут», «Убрать из пула», «Добавить контейнер резервом…» (выбор щелчком — для телефона), «подробнее».

Резервная цель нарисована пунктиром — и рамка, и связь, — с подписью «резерв»; цвет точки — по проверке, так что видно, можно ли на неё переключить. «Переключить трафик сюда» доступно только у резервной цели, живой по проверке; после короткого подтверждения «Переключить трафик с A на B?» эта цель встаёт в работу, остальные — в резерв: новых соединений не получают, открытые доживают. Обычный порядок — сначала добавить резервом, дождаться зелёной точки, потом переключить.

Защита: последнюю рабочую цель маршрута нельзя увести в резерв, перенести или убрать, убрать можно только резервную — карта объяснит почему.

Черновик (по умолчанию): действия копятся на карте — новые цели помечены «+ новая», изменённые — «→ в работу», «→ резерв», «изменена», убранные остаются бледными и зачёркнутыми. Вверху — «Применить изменения (N)» (дифф маршрутов и пометка к версии; одна новая версия и одно задание), «Шаг назад» и «Сбросить черновик (N)» — с вопросом «Сбросить изменения карты?» («Сбросить» / «Продолжить правку»); тот же вопрос — при выходе из правки с непустым черновиком.

«Применять сразу» (переключатель над картой, выключен по умолчанию): каждое действие сразу уходит заданием в очередь Traffic Fly; шаги колеса копятся и уходят одним заданием, когда колесо остановилось.

Пока задание идёт, на затронутых маршрутах и целях — спиннер с отсчётом, над картой — метка задания (щелчок — журнал); не выполнилось — красная метка со ссылкой на журнал. Операции ложатся на ту версию, что есть на момент выполнения, поэтому задания в очереди не мешают друг другу.

Правка на карте

Правка, история и применение ​

  • Идентификатор нового маршрута заполняется сам: из первого имени (git.example.com → git-example-com), иначе из подписи (кириллица — латиницей), иначе из вида и порта (tcp-2222); занят — с суффиксом -2. Своё значение можно вписать: недопустимые знаки исправляются на лету (заглавные — строчными, пробел, _, . — в -), под полем — что разрешено. Если «Сохранить» неактивна, рядом написано, чего не хватает.
  • Маршрут меняет администратор; запись — через окно с диффом и пометкой, каждая запись — новая версия. Две правки одновременно не перезапишут друг друга: вторая получит отказ.
  • «История» — разница между версиями и возврат любой из них.
  • «Сухой прогон» — задание собирает конфигурации шлюзов и хостов, проверяет ключи и адреса целей, но ничего не записывает.
  • «Применить» — задание по шагам: ключи шлюзов → ключи хостов и адреса целей → хосты (туннели, DNAT) → шлюзы (туннели, правила, маршрутизатор). Хосты раньше шлюзов: трафик пойдёт, когда туннели уже готовы. Хост или шлюз, которому больше нечего делать, снимает свои туннели и правила.
  • Узел, у которого конфигурация уже та же (и он это подтверждает), не перекладывается: в журнале — «конфигурация уже на месте», а по сертификатам — «действует до …, не перевыпускается» или «выпускается».
  • На хост ставятся wireguard-tools и nftables, если их нет; на шлюз — при установке edge.

Шлюз ​

«Добавить шлюз» открывает установку nkt-edge с ролью fly: имя и сертификат не нужны, 80/443 шлюза принадлежат маршрутам. Поэтому роль fly не совмещается с вебхуками, API и колбэками — для них нужен отдельный edge; проверки снаружи (probe) совмещать можно. Если на VPS включён ufw, шлюз сам открывает 51820/udp и порты маршрутов по имени.

События ​

Хаб раз в минуту сверяет сводки шлюзов и пишет в оповещения: цель недоступна / снова жива, в пуле нет живых целей, туннель упал / поднялся. Telegram, Slack, исходящие вебхуки и узел n8n получают их как остальные события.

Примеры ​

Сайт из контейнера с сертификатом на шлюзе ​

Контейнер shop-web-1 на хосте web1 отдаёт HTTP на порту 8080.

ПолеЗначение
ВидHTTPS по имени (443)
Именаshop.example.com
Сертификат на шлюзеда
Целиweb1 → контейнер shop-web-1, порт 8080
ПроверкаHTTP-запрос, путь /healthz

DNS shop.example.com → адрес шлюза; у провайдера VPS открыты 80 и 443/tcp. После «Применить» шлюз получит сертификат, http:// уйдёт на https://, приложение увидит адрес клиента в X-Forwarded-For.

MQTT ​

БрокерМаршрут
без TLS«TCP-порт» 1883 → контейнер брокера, порт 1883
TLS у брокера«TCP-порт» 8883 → брокер, порт 8883 (сертификат у брокера)
несколько брокеров на одном 8883«HTTPS по имени», порт 8883, имена mqtt1.example.com, mqtt2.example.com — шлюз выбирает брокер по имени (SNI) и TLS не расшифровывает; адрес клиента — только через PROXY protocol (EMQX и HiveMQ понимают, mosquitto — нет)
MQTT через WebSocket«HTTPS по имени» на путь брокера, можно с сертификатом на шлюзе

По порту брокер видит настоящий IP клиента без настроек.

STUN ​

«TCP+UDP-порт» 3478 → STUN-сервер, порт 3478. Шлюз не подменяет адрес клиента, поэтому STUN отвечает клиенту его настоящим внешним адресом — так и должно быть.

coturn (TURN) ​

TURN выдаёт клиентам relay-порты из диапазона, и каждый порт должен прийти на тот экземпляр coturn, который его выделил. Поэтому relay — диапазоном портов (поле «до порта»): порт сохраняется насквозь, а работает одна цель — первая живая, остальные — запасные.

МаршрутВидПортыЦель
turnTCP+UDP-порт3478coturn, 3478
turnsTCP-порт5349coturn, 5349 (TLS у coturn)
turn-relayUDP-порт, диапазон49152–65535coturn (тот же хост), без проверки

В turnserver.conf coturn должен объявлять клиентам адрес шлюза и тот же диапазон:

ini
listening-port=3478
tls-listening-port=5349
min-port=49152
max-port=65535
# адрес шлюза снаружи / адрес coturn в своей сети (контейнер, машина)
external-ip=203.0.113.10/172.18.0.5
realm=turn.example.com

У провайдера VPS откройте 3478/tcp+udp, 5349/tcp и 49152–65535/udp. Контейнер coturn удобнее держать в network_mode: host или с постоянным адресом в своей сети: адрес из external-ip должен совпадать с тем, куда шлюз отдаёт трафик.

В конвейере ​

Маршрут можно вести прямо из конвейера compose — блоком fly: (полный список полей — в Выкладках):

yaml
action: compose
compose:
  file: deploy/docker-compose.yml
  project: shop
  hosts: [web1, web2]
  fly:
    - route: shop               # маршрут в Traffic Fly
      gateway: fly-vps          # шлюз; можно не писать, если он один
      kind: https
      names: [shop.example.com]
      tls: gateway              # сертификат на шлюзе
      dns: plugin               # A-запись имён на шлюз — через DNS-плагин хаба
      service: web              # сервис compose
      target_port: 8080         # его порт в контейнере
      check: http
      check_path: /healthz
    - route: shop-mqtt
      kind: tcp
      port: 1883
      service: broker
      target_port: 1883

После успешной выкладки хаб:

  • находит контейнер сервиса на каждом хосте выкладки (по меткам compose, свежим сканом хоста);
  • маршрута нет — создаёт его с этими целями и применяет;
  • маршрут есть и цели те же — ничего не перекладывает, только проверяет: цели живы, сертификат действует (контейнер пересоздан с другим адресом — хост сам переписал правило при том же скане);
  • цели поменялись — добавляет новые и выводит прежние (новых соединений не получают, открытые доживают), ждёт, пока новые пройдут проверку (до 90 с), и только тогда убирает прежние; не ожили — прежние остаются в работе, в журнале — предупреждение;
  • вид, имена, порты и сертификат существующего маршрута не перезаписывает: отличие от описания — предупреждение в журнале.

Сертификат на шлюзе при этом не перевыпускается: шлюз берёт сохранённый, а к Let's Encrypt обращается только когда до конца срока меньше 30 дней. «Сухой прогон» выкладки показывает, будет ли маршрут создан или сверен.

Перенос стека между хостами ​

Стек, к которому ведут маршруты, можно перенести на другой хост с карты хостов — маршруты переключатся на новый хост сами, A-записи и сертификаты не меняются. Пошагово на примере Forgejo — Перенос стека с Traffic Fly.

Вход в кластер Kubernetes ​

Цель маршрута — и машина-узел кластера: маршрут https на порт Ingress-контроллера всех узлов даёт вход в кластер с сертификатом на шлюзе и балансировкой по узлам — Кластер: от нуля до первого пода.

Без хаба ​

Хаб нужен для изменений, карты и оповещений. Сам трафик идёт без него — хаб можно выключить, он может быть недоступен или удалён:

  • Шлюз: служба nkt-fly хранит последнюю конфигурацию в /etc/nkt-edge/fly/fly.json и после перезагрузки поднимает из неё туннель, правила и маршрутизатор; проверки целей, балансировка, сертификаты на шлюзе и их продление — на нём же. Раз в минуту шлюз сам проверяет ключ WireGuard на интерфейсе.
  • Хост: туннели поднимает wg-quick@nkt-flyN из /etc/wireguard/nkt-flyN.conf. Раз в 30 секунд nkt на хосте сверяет адреса целей-контейнеров, машин и LXD со своим инвентарём: если контейнер пересоздан и получил другой IP, правило переписывается само.
  • Без хаба нет: правки маршрутов и «Применить», карты и сводок, оповещений.

Если хаба больше не будет, менять можно руками: на шлюзе — /etc/nkt-edge/fly/fly.json и sudo systemctl restart nkt-fly, на хосте — /etc/wireguard/nkt-flyN.conf и sudo systemctl restart wg-quick@nkt-flyN. Убрать: на шлюзе sudo systemctl disable --now nkt-fly wg-quick@nkt-fly, на хосте sudo systemctl disable --now wg-quick@nkt-flyN.

Если не работает ​

ПризнакЧто проверить
Шлюз «нет связи»edge на связи с хабом? systemctl status nkt-edge nkt-fly на VPS
«Применить»: шлюз или хост ответил ошибкой wg-quickв сообщении — строки его журнала и подсказка: AppArmor не дал прочитать ключ (обновите шлюз и хосты до 1.13.58) или не пустил sysctl и nft из wg-quick (до 1.13.62), ядро без WireGuard (VPS-контейнер — нужен KVM), порт занят
Цель «контейнера нет на хосте»контейнер удалили мимо хаба — в окне маршрута «убрать цель». Удаляя контейнер в «Контейнерах» хоста, хаб сам предлагает убрать его цели (галочка включена)
Сухой прогон: «ядро не поддерживает WireGuard» у хостахост — контейнер (openvz, lxc) или без модуля; туннель к нему не поднимется
Туннель не поднимаетсяпод шлюзом в «Шлюзах» написана причина: «хост ещё не настроен» — «Применить» до него не дошло; «шлюз не принимает» — хост шлёт, шлюз не принимает: sudo tcpdump -ni any udp port 51820 на шлюзе — пусто: закрыт 51820/udp у провайдера VPS или исходящий UDP у хоста; пакеты видны: файрвол шлюза или у интерфейса нет ключа (sudo wg show nkt-fly public-key → (none); шлюз 1.13.22+ ставит его сам); «ключи не сошлись» — «Применить» ещё раз; «туннель на хосте не поднят» — systemctl status wg-quick@nkt-flyN на хосте
«Хост не ответил за 20 с»открывается ли страница хоста в хабе; systemctl status netknownsthat на хосте
«nkt не умеет Traffic Fly»обновите nkt на хосте (нужен 1.13.12 или новее)
Цель «недоступна»порт цели и проверки; контейнер слушает на своём адресе, а не только на 127.0.0.1
Цель видит адрес 10.77.x.1маршрут по имени без PROXY protocol — включите галочку и настройте цель
Имя не открываетсяDNS имени указывает на адрес шлюза; на 443 нет другого сервера