Рік розробки власного VPN: стек, захисти та перші граблі після релізу

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Всім привіт.

Я Артем, Full-Stack Software Engineer. Майже рік соло роблю FollowNet VPN для iOS і розширення для Chrome: клієнт, бекенд, VPN-сервери, білінг, моніторинг. У травні коротко писав про нього на DOU, а зараз покажу технічний розбір. Розповім, як влаштований стек, які захисти стоять на API, і що реально ламалось у проді.

FollowNet уже є в App Store. На iOS: IKEv2, WireGuard, AmneziaWG, Hysteria2, DNS-профілі, auto-connect, Shortcuts, Smart Connect (підбір протоколу/сервера під мережу). У Chrome — браузерний проксі через SOCKS/gateway, без системного VPN на весь девайс.

Далі — як це зібрано і де боліло.

Архітектура та еволюція протоколів

Коли пишу «нода» / «VPN-сервер» — маю на увазі машину з VPN-протоколами (StrongSwan / WireGuard / Amnezia / Hysteria2), не Kubernetes. Спрощено потік такий:

Чому modular monolith

Я один. Один API на NestJS з domain-модулями зручніший за мікросервіси. Банально менше рухомих частин, простіше дебажити вночі. PostgreSQL (Prisma) — джерело правди. Redis — rate limit, бани, короткоживучий стан, pub/sub. FreeRADIUS — кого пускати у VPN і з якою групою (anon / free / premium).

Клієнти:

  • iOS — Swift/SwiftUI + Network Extension. Окремі tunnel-таргети під WG / Amnezia / Hysteria2 (Libbox); IKEv2 через системний NEVPNManager.
  • Chrome — розширення + SOCKS/gateway; сесія на сервері, бо в chrome.proxy нормальний auth-хедер зручно не прокинути.

VPN-сервери піднімаю Ansible-ом. Сайт і адмінка — Angular. Білінг: StoreKit/Apple на iOS, WayForPay на вебі.

Як з’являлись протоколи

Спочатку був IKEv2, щоб швидко завести на iOS без свого tunnel-процесу. Потім WireGuard, це простіший дата-плейн і контроль peer’ів; для звичайних мереж лишився базовим. Далі мережі, де звичайний UDP почувається погано → AmneziaWG і Hysteria2.

На клієнті протоколи розведені по окремим Network Extension. На старті так простіше дебажити. Ціна — дублювання спільної логіки (health-check, квота, reconnect). Якби починав зараз, мінімум зробив би одне ядро на Libbox.

Smart Connect — не просто «обери найближчий сервер». Дивлюсь на гео/мережевий контекст і набір правил (країна, ASN тощо), щоб не стартувати з протоколу, який у цій мережі часто відвалюється. З клієнта пишу телеметрію успіхів/фейлів; у адмінці бачу KPI і пропозиції правил по ASN — щось можна підкрутити руками, не чекаючи, поки набіжить купа тікетів. IKEv2 у меню лишається, але як перший автоматичний вибір я його не використовую.

Chrome є окремим шаром, не full-device VPN, а проксі в браузері.

Auth, підписки і доступ до VPN

Онбординг і сесія

На iOS можна стартувати як анонім та одразу перевірити конект, без реєстрації. Email / Google / Apple — пізніше.

На вебі пароля немає: лише код на пошту. Сесія — JWT на 180 днів, без refresh-токена. Для VPN-клієнта це зручніше, ніж викидати людину кожні кілька днів.

Три різні «доступи»

Це не одне й те саме:

  • App JWT — API (профіль, конфіги, підписка);
  • RADIUS username/password — кого пускає VPN-сервер (зокрема IKEv2);
  • для Chrome — окремий proxy JWT (~4 год) під SOCKS/gateway (інший purpose, коротший TTL).

Реєстрація потрібна, щоб:

  • пройти до оплати підписки;
  • підняти тижневий ліміт з 512 МБ (анонім) до 2 ГБ (зареєстрований free).

Ліміт тижневий, з понеділка 00:00 UTC лічильник знову з нуля. У Premium окрема політика. Пристрої можна 2 на free, а 5 на premium/trial. Якщо слотів немає, то витісняється найстаріший за lastSeenAt.

На Chrome з Sign in with Apple окрема історія. Apple не приймає нормальний redirect на chromiumapp.org, тому relay через API (extension-callback).

Білінг і синхронізація доступу

Підписка живе в Postgres, але рядок premium сам тунель не відкриває. Стан треба донести до RADIUS-груп і політики на VPN-сервері.

  • iOS — StoreKit 2 + Apple Server Notifications (перевірка JWS);
  • веб — WayForPay (HMAC-підпис + ідемпотентність). Чекаут по email, не через JWT мобільної апки: юзер уже має існувати в базі.

Два різних «бани»:

  • banned на акаунті — ріже логін і VPN;
  • тижневий traffic ban (особливо на проксі) — до наступного понеділка UTC.

Найчастіший біль — це роз’їзд станів. У API підписка нова, на RADIUS ще стара (або навпаки). Тому cron’и на протухлі підписки й синхронізацію доступу, плюс sessionId, щоб старий disconnect не вбивав новий конект після reconnect.

Щоб клієнт не жив зі старим станом підписки, є Socket.IO: бекенд пушить оновлення (підписка, ліміт тощо).

Коротко: доступ — це ланцюжок акаунт → підписка → RADIUS / VPN-сервер → тунель, а не одна галочка в API.

Захисти на бекенді

Повного «анти-DDoS на всі випадки» не обіцяю. Є захист від абузу API і типових дірок.

HTTP API

Helmet, CORS allowlist, ValidationPipe на DTO, middleware на розмір JWT, жорсткіша валідація на публічних ендпоінтах.

Rate limit

Лічильники в Redis (не в пам’яті процесу), контексти global / login / register / admin_login.

Приклад для global: 100 запитів / хв з IP. Після повторних порушень — escalating ban: спочатку без бану / коротший, далі 10 хв → 1 год → 24 год. Окремо cooldown на повторну відправку email-коду. Anon/реєстрація теж під лімітом — інакше легко нагенерити сміття на API й RADIUS.

Сервіс + Виклик

// Redis sliding window + escalating ban (ядро)
async checkRateLimit({ ip, type, windowMs, limit, banDurations }) {
  const key = `${type}:timestamps:${ip}`;
  const banLevelKey = `${type}:ban:level:${ip}`;
  const activeBanKey = `${type}:ban:active:${ip}`;
  const now = Date.now();
  if (await this.redis.get(activeBanKey)) {
    throw new ForbiddenException({ code: `${type.toUpperCase()}_BANNED` });
  }
  await this.redis.client.zadd(key, now, `${now}`);
  await this.redis.client.zremrangebyscore(key, 0, now - windowMs);
  const count = await this.redis.client.zcard(key);
  if (count > limit) {
    const level = parseInt((await this.redis.get(banLevelKey)) ?? '0', 10) + 1;
    await this.redis.set(banLevelKey, String(level), 7 * 24 * 3600);
    const banDuration =
      banDurations[Math.min(level, banDurations.length - 1)];
    await this.redis.set(activeBanKey, '1', banDuration);
    throw new ForbiddenException({ code: `${type.toUpperCase()}_RATE_LIMIT` });
  }
}
// global: 100 / хв, бани 0 → 10хв → 1год → 24год
await this.checkRateLimit({
  ip,
  type: 'global',
  windowMs: 60_000,
  limit: 100,
  banDurations: [0, 600, 3600, 86_400],
});
// anon/register: 2 / добу
await this.checkRateLimit({
  ip,
  type: 'register',
  windowMs: 24 * 60 * 60 * 1000,
  limit: 2,
  banDurations: [24 * 60 * 60],
});
// admin login: 5 / 15 хв
await this.checkRateLimit({
  ip,
  type: 'admin_login',
  windowMs: 15 * 60 * 1000,
  limit: 5,
  banDurations: [10 * 60, 60 * 60, 24 * 60 * 60],
});

Пошта й акаунти.

Disposable-email блоклист + оновлення по cron. Статус banned перевіряється на JWT і на логіні. Ліміт пристроїв — теж антиабуз.

Трафік

Якщо тижневий ліміт вичерпано, це бачить облік/RADIUS, API ініціює disconnect:

  1. сповіщення клієнта (щоб UI показав ліміт);
  2. RADIUS CoA на VPN-сервер (різні порти/агенти для IKEv2 і WG/AWG/Hy2);
  3. якщо CoA не пройшов для WireGuard/Amnezia — запасний шлях: прибрати peer по SSH лише на відому NAS-адресу цієї сесії. Для IKEv2 і Hysteria2 SSH-fallback не використовую. Це не «бекенд SSH-ить інтернет», а вузький fallback, коли CoA не відпрацював.
  4. для Chrome/proxy — traffic ban у Redis до наступного тижневого вікна.

Webhooks і службові виклики

Apple — перевірка JWS. WayForPay — HMAC-MD5 підпису за їхньою схемою; без валідного підпису callback відхиляється, плюс ідемпотентність повторів. Ноди й скрипти ходять в API з internal secrets, не з юзерським JWT.

Клієнт

Certificate pinning. Bootstrap з кількох API-дзеркал (.com / .net): якщо одне недоступне — клієнт пробує інше. На VPN-серверах — firewall, CoA лише з очікуваних адрес. Адмінка й сайт окремо від VPN-датаплейну.

Rate limit тримає брут і спам реєстрацій. Від великих мережевих атак на IP VPN-сервера цього мало — там рівень провайдера.

Як я розумію, що щось впало

Без телеметрії VPN швидко стає ворожінням. Якщо «не конектиться», це клієнт, API, сервер чи мережа юзера?

  • Клієнт: Firebase Analytics + Crashlytics (окремо важливі падіння Network Extension).
  • API / сервери: Prometheus + Grafana (конекти, rate limit, CoA, підписки, бекапи) + sync статистики з нод у API.
  • Ops: Angular-адмінка (юзери, сервери, сесії, тікети, алерти по портах) і Telegram для швидких сигналів.
  • Бекапи: pg_dump → gzip → GCS, з метрикою останнього успішного прогону.

Кореляція між шарами важливіша за кількість графіків.

Grafana


Admin Panel

Що ламалось у проді

Кейс 1. Connected, а інтернету немає

Тунель «зелений», трафік не йде. На Hysteria2 траплялось: uplink є, downlink ні; або ліміт уже вичерпано, а UI ще щасливий.

Що зробив: health-монітори в extension’ах + перевірка квоти на старті тунелю.

Кейс 2. API віддав конфіг, VPN не піднявся

Конфіг ≠ доступ. Далі RADIUS і VPN-сервер: група, пароль, облік, CoA. Типово, коли підписка в API нова, на RADIUS стара, або peer після ліміту не відрізало.

Що зробив: ретраї CoA, окремий coa-агент на сервері, SSH-fallback лише для WG/AWG, sessionId проти гонок reconnect.

Кейс 3. Handover і хибні health-check’и

LTE ↔ Wi‑Fi і короткі діри в мережі. Якщо одразу рвати тунель — флаппінг. Тому grace period’и: занадто м’яко — довго сидиш у мертвому конекті; занадто жорстко — сам провокуєш відвали.

Кейс 4. Деплой протоколів на серверах

Ansible зменшив ручну роботу, але AmneziaWG умів розтягуватись на довгі інстали з ребутами. iptables треба тримати так, щоб правила не зносило. Кожну нову машину — у FreeRADIUS clients, інакше IKEv2 ловить EAP-таймаути. Параметри Amnezia на сервері й у Postgres для клієнта мають збігатися.

Кейс 5. Кілька дзеркал API

Один домен API — слабке місце: у частині мереж хост недоступний, апка виглядає зламаною. Bootstrap .com / .net + pinning зняв частину таких кейсів. Проблема була в дорозі до API, не в VPN-тунелі.

Кейс 6. Chrome як окремий продукт

Немає system VPN API як на iOS. Тому SOCKS + TLS gateway, коротка proxy-сесія, Apple Sign-In relay через бекенд. Багато речей, які на iOS дає Network Extension, у розширенні збирав вручну.

Кейс 7. Apple хоче рефанд — і питає, чи юзер взагалі користувався

Коли юзер просить повернути гроші, Apple шле вебхук на мій API. Там буває не лише REFUND, а ще CONSUMPTION_REQUEST — типу «розкажи, чи людина взагалі користувалась».

На CONSUMPTION_REQUEST я дивлюсь, скільки в нього було VPN-сесій, і відповідаю Apple окремим запитом (PUT на Consumption API): якщо сидів нормально — прошу не повертати (DECLINE), якщо майже не чіпав — ок на повний рефанд (GRANT_FULL).

Коли приходить уже REFUND — знімаю premium і оновлюю RADIUS, щоб після повернення грошей VPN одразу не працював.

Що б зробив інакше

  • Раніше — єдину модель сесії між клієнтом, API і VPN-сервером.
  • На клієнті — одне ядро на Libbox, а не кілька окремих Network Extension.
  • Спочатку стабілізувати WireGuard + облік трафіку, потім Amnezia/Hysteria2.
  • Більше синтетичних проб портів/протоколів з серверів, менше реакції лише на тікети.
  • Жорсткіший контур секретів для службових скриптів і FreeRADIUS.
  • Коротку внутрішню документацію «чому саме так» — з перших місяців.

Висновок

Три речі, які б я сказав собі рік тому:

  1. API «ок» ≠ VPN працює. Завжди проєктуй ланцюжок до RADIUS/сервера, не лише видачу конфігу.
  2. Connected ≠ є інтернет. Health-check і квоту треба в тунелі, не тільки в UI.
  3. Не плодити протоколи й NE-таргети рано. Спочатку один стабільний шлях і облік трафіку — потім решта.

Якби починав зараз: monolith лишив би, але сесію й клієнтське ядро заклав би інакше з першого місяця. Якщо хочете подивитись, сайт тут: follow-net.com

Фідбек цікавий і від інженерів, і від тих, хто крутив свій продукт. Що б ви перепроєктували першим? Які ризики на VPN-серверах найбільш болючі при зростанні? Які anti-abuse контроли додати далі? Як би ви крутили маркетинг і дистрибуцію для соло VPN (ASO, контент, партнерки)?

👍ПодобаєтьсяСподобалось17
До обраногоВ обраному8
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Не розумію, чому стільки негатива в коментах — чудовий і технічно-складний проект. Можливо потрібно було назвати статтю не «vpn», а «vpn-service», тоді не було б сподівання побачити розробку якихось протоколів з нуля. А так то круто, тим більше для соло.

Дякую за підтримку

Особисто я користуюся Freedome, але бажаю успіху вашому проєкту!

Дякую за підтримку!

Якщо цікаво спробувати FollowNet — напиши в особисті, дам підписку.

VPN-сервери піднімаю Ansible-ом

Цікаво де піднімаються і що це обходиться фінансово?

Зараз ноди на OVH, Hetzner і IONOS.

По грошах виходить орієнтовно ~$100/міс на інфру (VPS під VPN + бекенд). Поки серверів ще мало, тому сума така. Далі залежить від кількості нод і трафіку.

Якщо знаєте нормальні й недорогі хостинги під VPN-ноди — киньте в коменти, буду вдячний.

Роскомнадзор уже забанил ваши сервера, или ещё пока что нет? Я в статье увидел всё что угодно, кроме решения ключевой проблемы — блокировки вашего сервиса. Если это никак решаться не будет, ваш vpn-сервис никому не будет нужен.

Вы можете например на каждый VPS повесить несколько IP-адресов, и далее каждому новому юзеру выдавать уникальный набор IP-адресов, на которые он может коннектиться. Далее по забаненным IP-адресам можно например вычислять проблемных юзеров.

Я думаю кінцева мета ,не обхід заблокованих ресурсів,а просто vpn сервіс,декілька апішок на віпісішку-ну тут я думаю 50 на 50,бо як правило на практиці можуть блокати не на рівні айпішки окремої а на рівні цілої підмережі,ASN і тд. тому я думаю це просто фікс ніж стратегія.В загальному якщо уже постане питання уникнення блокування то я думаю тут доречна стратегія зробити окрему інфраструктуру з постійною ротацією вузлів, дзеркалами, доставкою конфігів і моніторингом блокувань.Але це лише моя суб’єктивна думка.;-)

Частково вже так. Дехто з РФ підключається і все ок. У більшості тунель піднімається, а трафіку немає — типова картина під DPI/блоками.

FollowNet я під РФ і РКН не робив.

  1. Ціль — не ринок країни-агресора. Під постійні бани сервіс не заточував.
  2. На iOS оплата йде через App Store. У РФ з цим погано: обмеження оплат і акаунтів, тож будувати продукт «під РФ» через StoreKit майже без сенсу.
  3. Ідея з пулом IP на юзера в цілому нормальна. Але РКН часто ріже не один IP, а цілі підмережі / хостинги. Знайомі з iOS-чату, у кого VPN під РФ — кажуть, що дзеркала міняють десятками на день, бо все швидко лочать. Це вже окрема операційка, не «додав кілька адрес на VPS». Я туди свідомо не йду.

Питання банів може стосуватися не юзерів, а того куди юзери ходять.

Дякую чате, а тепер запакуй в пдф будь ласка

За це взагалі хтось платить?

Так. Поки скромно — продукт свіжий, але підписки вже є. Free → Premium працює, просто воронка ще розкачується.

Враховуючи що кожен блогер початківець рекламує впн сервіс, а кожен продвинутий блоегр мілліоник рекламує власний впн сервіс, причому що вітчизняні що зарубіжні, то платять і не мало. Або це якийсь гарний спосіб відмивання грошей. Інакше не розумію як це пояснити.

Враховуючи нещодавній звіт про android vpn — єдиний сенс — вивужувати платіжні дані через якусь дірку в протоколі, а також продавати стату різним органам.
У будь-якому випадку погана справа, кому треба реально — сетапит собі сам на фрі тайр.

Рік розробки власного VPN:

рік розробки обгортки для vpn протоколів які розробили інші і віддали у опенсорс

Це правда. І я дуже вдячний open-source спільноті за ці протоколи. Моя робота була в тому, щоб зробити з них стабільний, захищений і монетизований продукт, який працює в продакшні. Саме про цей досвід — весь мій пост.

У нас тут технічний чят все ж таки, і тому коли читаєш «новий впн» очикуєшь щось більше джаваскірпту та графани. Але я можу помилятися

А що не так? Ви правда думали людина рік в одне лицце напише щось нове щось на рівні мережових протоколів? ))) Без образ але трохи наївно звучить. То був би якись хрестойо...сі плюс плюсинк із деприваціює сна на адералі, 40 років роботи в розгятнутому світері і клавіауторю міцумі і з баночка нескафе попільничкою)))

yggdrasil «просто існуе»
і його «створили» меньше чим за рік, правда розвивается і допилюеться досі, але як факт того що новий мережевий стек «за рік» можна створити і навіть без плюсів і десятків років досвіду с сфері

Практично усе сучасне айті це взяти напів готові апішки і зклепати з них готовий продукт. Низькорівневі штуки зараз дуже нішеві і цим займаються не в одне лице і завичай споносрами є великі корпорації, той самий опенсурс за ці кошти і живе. Але і це вже автоматизуєця тим самим ШІ+агенти, котрі так само напів готові апішки :-)

Дякую за статтю. Дуже багато всього цікавого і нового для мене. Хотілося б цей матеріал розбити на декілька частин і більше деталей і нюансів побачити))

Підписатись на коментарі