Traffic Fly: вход из интернета через VPS
Сервис стоит дома или в офисе за NAT, а пользователям нужен адрес в интернете. Traffic Fly принимает трафик на дешёвом VPS с белым адресом и отдаёт его по туннелю WireGuard на хосты — в контейнер, в виртуальную машину, в LXD или в службу самого хоста. Клиент виден целям под своим настоящим IP, работают TCP и UDP, а на одном порту можно держать пул целей с балансировкой и проверками.
Настраивает всё хаб — в разделе меню «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, выбор по заголовку Host | PROXY 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 — диапазоном портов (поле «до порта»): порт сохраняется насквозь, а работает одна цель — первая живая, остальные — запасные.
| Маршрут | Вид | Порты | Цель |
|---|---|---|---|
turn | TCP+UDP-порт | 3478 | coturn, 3478 |
turns | TCP-порт | 5349 | coturn, 5349 (TLS у coturn) |
turn-relay | UDP-порт, диапазон | 49152–65535 | coturn (тот же хост), без проверки |
В turnserver.conf coturn должен объявлять клиентам адрес шлюза и тот же диапазон:
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: (полный список полей — в Выкладках):
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 нет другого сервера |