Як працюють мережеві розширення macOS: досвід створення FireWally
Привіт! Мене звати Олександр Рапчинський, я розробник у Nektony. Мені 24, у компанії я вже пʼять з половиною років: прийшов ще на другому курсі і за цей час встиг попрацювати чи не над усіма нашими продуктами. Проєктів у нас багато, команда невелика, тож доводиться розбиратися в кожному.
Майже всі наші утиліти орієнтовані на звичайного маковода й працюють із файлами: щось знайти, порахувати, акуратно видалити. Але минулого року ми побачили, що для нас може бути місце й на ринку фаєрволів. Робота з мережевим трафіком була для нас абсолютно новою темою, і її довелося глибоко досліджувати з нуля. Так зʼявилась FireWally: безкоштовна апка, яка показує, які програми на Mac ходять в інтернет і скільки трафіку споживають, та дозволяє одним перемикачем заборонити будь-якій із них доступ до мережі.
У цій статті розкажу, як влаштовані мережеві розширення macOS очима розробника, який пройшов шлях від «ніколи не працював із трафіком» до релізу в App Store: що за архітектуру нам пропонує Apple, де ми набили ґуль, чому в користувачів фаєрволів інколи зникає інтернет і що б я порадив тим, хто збирається будувати щось на Network Extension.
Звідки взялась ідея і хто це робив
На ринку фаєрволів для macOS насправді небагато великих гравців. Є Little Snitch, платний і з безкоштовним режимом, і є опенсорсний LuLu. По суті, два помітні продукти на всю нішу. Ми побачили можливість зайти в неї теж: зробити якісно, у нашому стилі, з простим і зрозумілим інтерфейсом.
Команда була компактна: продакт, дизайнер і три програмісти, з яких я був лідом і робив майже все, а двом джунам ми віддавали задачі по UI. Плюс наші тестувальники, які потім ганяли фаєрвол. Від ідеї до першого релізу минуло приблизно пів року, але маю уточнити, що весь цей час ми займалися не лише FireWally. У мене паралельно було чимало задач по інших проєктах, тому робота розтягнулася.
Найбільше на старті я боявся самої теми. Здавалося, доведеться вивчати, як працює інтернет на супернизькому рівні: пакети, протоколи, вся ця теорія. На практиці виявилося, що усі наші потреби покриваються готовими рішеннями Apple. Що справді забрало час, так це нова для нас модель застосунку: звʼязка процесів, які мають постійно взаємодіяти між собою.
Про неї далі й піде мова.
Що Apple дає розробнику фаєрвола
Колись сторонні фаєрволи на Mac жили в ядрі системи через kernel extensions. Із сучасними версіями macOS єдиний легальний шлях працювати з чужим трафіком лежить через фреймворк NetworkExtension. Ти отримуєш від системи готовий набір інструментів, а Apple натомість бере на себе відповідальність за безпеку цієї схеми.
Інструментів там кілька, під різні задачі. Є проксі-провайдери, які пропускають трафік крізь себе. Є фільтрація на рівні окремих пакетів даних. А є найпростіший варіант: фільтр на рівні зʼєднань, який бачить кожне нове підключення і виносить йому вердикт. Ми перебрали варіанти й зупинилися саме на ньому, бо він покрив усі наші вимоги.

До речі, про конкурентів. LuLu відкритий, тож із його кодом можна просто ознайомитися. Про Little Snitch багато що видно за зовнішніми ознаками: у файлах застосунку, у plist, у тому, які entitlements він використовує. І висновок цікавий: усі ми, включно з FireWally, використовуємо один і той самий різновид Network Extension. Ядро в усіх фаєрволів на macOS фактично спільне. Відрізняється лише логіка всередині: які процеси й протоколи відстежуються, як групується трафік, як ухвалюються рішення.
Апка і розширення: два процеси, один продукт
Технічно FireWally складається з двох частин. Перша частина: основний застосунок, наш UI (іконка в менюбарі, список програм, цифри трафіку, перемикачі). Друга: сам Network Extension, окремий процес, який і працює з трафіком. Усередині цього розширення ми реалізуємо свою логіку: прописуємо правила, у яких випадках трафік пропускається, а в яких — ні. Тобто система дає каркас, а поведінку ти кастомізуєш сам.

І ось тут почалося найскладніше в усьому проєкті: навчити ці два процеси спілкуватися. На Mac існує чимало способів міжпроцесної взаємодії: спільні файли, darwin-нотифікації тощо. Але виявилося, що між застосунком і мережевим розширенням є рівно один легальний робочий канал: XPC Connection. З ним і треба було розібратися: як його правильно підняти, як взаємодіяти, як обробляти обриви.
Друге складне питання: оновлення. Виходить нова версія застосунку, а в системі вже висить розширення попередньої. Як саме система його замінює? Як переконатися, що стара версія не залишилася висіти? Як апці зʼєднатися вже з новим розширенням? Нюансів виявилося дуже багато, і саме ця тема стала найбільшим головним болем усієї розробки. Жодного ноу-хау в наших рішеннях немає, це звичайні інженерні підходи, просто їх треба було застосувати в правильному місці й у правильний момент.
Життя одного зʼєднання
Тепер про те, як узагалі виглядає робота фільтра. Коли якась програма створює нове зʼєднання, система дає нашому розширенню шанс винести вердикт ще до того, як зʼєднання фактично встановиться. Варіантів кілька: пропустити повністю, одразу перервати, поставити на паузу на певний час або попросити в системи трохи даних із цього зʼєднання, подивитися на конкретні байти й ухвалити рішення вже після цього. Спершу вердикт, потім трафік.

З цього випливає головна метрика, за якою я стежив увесь час розробки: затримка. Від моменту, коли зʼєднання прийшло у FireWally, до моменту нашого рішення минає якийсь час, і цей час має бути таким, щоб користувач узагалі не здогадувався, що кожне його зʼєднання окремо обробляється. З цим, на щастя, проблем не виникло.
Ще один нюанс: наш фільтр працює з двома протоколами, TCP та UDP, і блокування для них відчувається по-різному. Якщо перервати TCP-зʼєднання, програма одразу розуміє, що звʼязку немає. А UDP влаштований так, що втрачати частину трафіку для нього нормально, тому заблоковане UDP-зʼєднання виглядає радше як зависання: наприклад, відео просто зупиняється і далі не вантажиться. Ми в цій ситуації чесно виконали волю користувача, а як чужий застосунок обробляє недоставлені запити — це вже відповідальність його розробників.
Чий це трафік? Несподівано складне питання
Здавалося б, що тут складного: бачиш зʼєднання, бачиш процес, показуєш назву програми. На практиці це одна з найзаплутаніших частин фаєрвола.
По-перше, застосунки люблять запускати власні допоміжні процеси, і в інтернет ходять саме вони. Якщо показувати кожен процес окремо, користувач побачить Google, окремо Google Helper і ще з десяток процесів того ж Google під кожну вкладку. Незрозуміло і некрасиво. Тому трафік треба групувати: брати процес, підніматися вгору по ланцюжку його батьків до основного застосунку і відносити трафік до нього.
По-друге, система не для всіх процесів дозволяє нормально визначити батьківський застосунок. Є приватне API, яке цю задачу вирішує краще (приватне не в сенсі чуже: це системні функції Apple, просто не відкриті для розробників). Але App Store на ревʼю автоматично сканує застосунок на використання таких речей, тому ми свідомо від нього відмовилися і взяли простішу схему. Вона покриває не всі 100% випадків, але такою є ціна за присутність в App Store.

По-третє, системні процеси. У macOS багато служб, які ходять в інтернет, і в більшості з них незрозумілі пересічному користувачу назви. Ми винесли їх в окрему групу System і показуємо сумарний трафік. Але є системні процеси, які юзеру якраз знайомі й зрозумілі (наприклад, Siri), і їх хочеться показувати окремо. Тож у нас зʼявилася окрема постійна задача: вирішувати, які системні процеси заслуговують на власний рядок, а які залишаються частиною спільної групи.
Найгірший баг: коли просто зникає інтернет
А тепер про найнеприємніше, з чим ми зіткнулися. Інколи в користувача просто пропадав інтернет. Без видимої причини чи закономірності.
Коли я почав у цьому копатися, зʼясувалося, що конкретної провини на нашому боці немає: нічого страшного з трафіком ми не робимо. Проблема живе на боці системи: мережеві розширення самі по собі працюють не завжди бездоганно, особливо на старіших версіях macOS. Достатньо погуглити, щоби побачити скарги на зникнення інтернету практично для всіх популярних фаєрволів на Network Extension.
У підсумку довелося, скажімо прямо, замазувати баги системи. Після перезавантаження Mac ми перевіряємо, чи є в користувача інтернет, і якщо бачимо, що його немає, до трьох разів перезапускаємо власний фаєрвол. Звучить як шаманство, але за цим є логіка: коли Network Extension активується або деактивується, система перемикає всі наявні з’єднання і запускає їх заново, вже через фільтр. Умовно, ви дивитеся YouTube, вмикається фаєрвол, і ваше відео тепер має пройти через нього. Тому повне перемикання трафіку інколи лікує ситуацію. На жаль, не у 100% випадків.
Є й друга варіація цієї проблеми: коли на одному Mac встановлено багато різних мережевих розширень від різних продуктів, з інтернетом теж може початися біда. І тут ми взагалі мало що можемо: на нашому боці проблем немає, а чужі розширення ми не контролюємо. З такими ситуаціями доводиться змиритися і чекати фіксів платформи.
Продуктові рішення: App Store і мінімалізм першої версії
Ми з самого початку планували саме апсторну версію, бо до App Store у користувачів історично вища довіра, тож успіх там гарантує і успіх на інших майданчиках. Сайтова версія в нас теж готова, а бету ми свого часу навіть викладали на Reddit, щоб перевірити, чи все гаразд перед релізом.

Побоювань, що застосунок не пройде ревʼю в App Store в мене не було, адже ми будуємо продукт на фреймворку, який Apple сама створила й вітає його використання. Ревʼю ми пройшли з другої спроби: у першій не виконали одну невелику вимогу гайдлайнів, просто продивилися момент, виправили і з другого разу нас досить швидко прийняли.
Друге помітне рішення: FireWally не показує домени та адреси, куди саме ходять програми. Тільки який саме застосунок ходить і скільки трафіку. Це усвідомлений вибір для першої версії: ми розглядали її як MVP і не стали заглиблюватися. У якийсь момент ми навіть показували кількість доменів для кожної програми, без самого списку, але зрештою відмовилися й від цього. Наша концепція: усе має бути красиво і зрозуміло. Вмістити в такий інтерфейс усе розмаїття функціоналу, яке хочеться, — це важка UI/UX-задача: навіть для MVP ми готували PRD і дизайн майже два місяці.
Ми вважаємо, що принцип захисту свого Mac для пересічних користувачів має бути простим: не чіпай того, чого не розумієш і що тебе не турбує. Якщо умовний калькулятор надсилає великі обсяги даних на дивний домен, це привід насторожитися. А от глибоке розуміння мережі для базової безпеки не потрібне, і наш інтерфейс має це враховувати.
Олімпіадне минуле, оптимізація і ШІ в розробці
До роботи я займався олімпіадним програмуванням, суть якого — вичавити з програми максимум швидкості за мінімуму памʼяті. Звідти в мене залишилася жорстка звичка стежити за ресурсами в кожній зміні коду. Можливо, від неї іноді варто було б відмовлятися заради економії часу, але для фаєрвола вона виявилася доречною: коли тобі дають робити продукт з нуля, можна закласти оптимальність архітектурно, і в першій версії FireWally з цим усе досить добре.
Конкретний приклад без жодних ноу-хау: розширення віддає застосунку дані в реальному часі, і коли приходить новий трафік, ми не перебудовуємо весь список програм, а точково оновлюємо одне значення для одного застосунку. Просто акуратність і відсутність зайвих рухів. Для процесу, який висить у фоні постійно, це і є оптимізація: мінімум процесора та мінімум памʼяті.
Цікавий момент про інструменти. Коли писався FireWally, потужних агентних ШІ-асистентів ще не було, з доступного був умовний ChatGPT, тож усе писалося руками, з періодичними консультаціями. Уже постфактум ми проганяли готовий код через Claude у пошуках проблем із памʼяттю, зациклюваннями чи потенційними крешами. Великих відкриттів він не зробив: підправив кілька ризикованих місць, де теоретично міг статися креш, і загалом оцінив кодову базу високо.

До речі, про ШІ я інколи думаю: чи не забере він аудиторію в таких утиліт? Якщо говорити лише в контексті задачі аналізу трафіку, то я маю сильні сумніви. Фаєрвол вимагає постійного системного моніторингу за правилами, зі складною логікою всередині. У теорії ШІ може написати собі такий інструмент, але поки що хвилі згенерованих ним фаєрволів в App Store не видно. Очевидно, це не так просто, як здається.
Що б я порадив тим, хто піде цим шляхом
Насамкінець кілька настанов, які я виніс із цього проєкту і які, сподіваюся, комусь заощадять час.
Прочитайте документацію фреймворка повністю, а не фрагментами під поточну задачу. Я спершу читав лише те, що було потрібно просто зараз, і через це робота з розширенням ішла повільніше, ніж могла б: повна картина того, що фреймворк уміє і чого не вміє, зекономила б мені чимало часу.
Якщо ваше рішення включає extension, закладайте багато часу на його обвʼязку. Налагодити спілкування з розширенням, його оновлення разом із версіями застосунку, відновлення звʼязку: усе це виглядає другорядним, а насправді стало найскладнішою частиною проєкту.
І будьте готові до того, що з Network Extension періодично щось відвалюється: то інтернет, то швидкість. Такі розширення сидять на досить низькому рівні системи, у них трапляються внутрішні проблеми, які ви не контролюєте, і будь-який баг на цьому рівні боляче відгукується. Це ціна за можливість легально працювати з чужим трафіком на macOS.
Якщо ви робили щось на NetworkExtension і ваш досвід відрізняється, буде цікаво почитати в коментарях. А якщо тільки плануєте, питайте: розкажу, що знаю.
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую за статтю !
Особливо про обв’язку XPC та оновлення system extension, це справді жесть.
Кілька доповнень з досвіду macOS-розробки:
1. Про «чий трафік»: окрім приватного API та ланцюжка батьків є публічний шлях. З macOS 13 у NEFilterFlow є sourceProcessAuditToken: коли з’єднання від імені застосунку створює системний процес, він відрізняється від sourceAppAuditToken. А групувати зручно за підписом коду: SecCodeCopyGuestWithAttributes з kSecGuestAttributeAudit дає Team ID, а шлях до зовнішнього .app ловить хелпери й XPC-сервіси з бандла — зокрема ті, що запускає launchd і для яких ланцюжок батьків обривається на PID 1.
2. Про домени: приклад з «калькулятором, що шле дані на дивний домен» якраз аргумент показувати домени хоча б по кліку — без цього користувач його не помітить.
3. Про зникнення інтернету: було б дуже цікаво побачити цифри — як часто допомагає перезапуск і як ви відрізняєте «мережі ще немає після завантаження» від «з’єднання не проходять» (NWPathMonitor). І як ви захищаєте шлях вердикту від блокувань, якщо розширення не встигає з вердиктом.
Успіхів FireWally!