Эта статья о том, как я хотел сэкономить несколько секунд при переключении системного прокси в Nekobox, а в итоге уже несколько месяцев пишу мини-программу для управления sing-box.
Началось с того, что для прокси на Windows я стал использовать Nekobox. Про гибкое раздельное туннелирование я еще не знал, и приходилось постоянно включать и выключать системный прокси, чтобы зайти то туда (сайт заблокирован), то сюда (сайт блокирует IP прокси). Много раз в час: клик по значку в трее, режим системного прокси, отключить (а потом обратно). И я подумал, что было бы удобнее просто кликать по значку. Ничего сложного — почему бы не реализовать? Начал я, конечно же, с рисования значка. Решил, что хорошо подойдет портал из «Рика и Морти» как метафора беспрепятственного перемещения между измерениями. Провел целый вечер в Procreate на iPad, замучился, устал и отложил затею на потом.
В следующие полгода прокси намного глубже вошли в мою жизнь: клиент на телефон и телевизор, хитрая маршрутизация, разные программы, сложные конфиги. Я понял, что Nekobox — это просто красивая обертка вокруг sing-box для новичков. В конце концов я пришел к оригинальному sing-box и к тому, чтобы на всех устройствах использовать общую конфигурацию. Проблема только в том, что оригинальный sing-box на Windows — это консольное окошко без управления. Пришло время вернуться к моему зеленому порталу. Программу я решил писать на Delphi (вспомнить былое). Значок в трее, запуск ядра, системный прокси по клику — задача на один вечер. Так появился sing-box-drover. Как всегда, учел я не всё.

Версия 0.1: самое начало
Программа на один вечер, поэтому делаем только самое основное. Используем Mutex, чтобы нельзя было запустить две копии программы. Создаем значок в трее через TTrayIcon. Принцип переключения системного прокси через WinAPI подсматриваем в Nekobox.
Запускаем невидимое ядро sing-box при старте программы через CreateProcess с CREATE_NO_WINDOW (никаких черных окошек). Остается проблема, что если убить нашу программу, то ядро незаметно продолжит работать. Решение подсказала LLM. Создаем JobObject с флагом JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, добавляем процесс sing-box в эту «работу». Как только закрывается последний Handle на этот JobObject (в том числе при аварийном закрытии нашей программы), система принудительно завершает все привязанные процессы.
Еще я добавил возможность переключения селекторов через выпадающее меню в трее. Как их переключать? Sing-box может принимать команды через Clash API (изначально появился в программе Clash, стал своеобразным стандартом, который реализуют многие «прокси-платформы»). По-хорошему, список доступных селекторов тоже нужно было получать через этот же API, но мне показалось более простым решением читать список из конфига самостоятельно, а через API только переключать. На решение повлияло и то, что конфиг я уже читал для определения настроек mixed inbound (какие IP и порт указывать при настройке системного прокси). В итоге нам нужно читать конфиг, находить там настройки Clash API (IP, порт, пароль), читать список селекторов со значениями, рисовать меню, дергать API при нажатиях. С API работаем через THTTPClient в отдельном потоке, чтобы не подвешивать интерфейс (каждый раз запускаем новый поток и за ним не следим; сам умрёт и освободит память). Тонкость: у этого THTTPClient явно отключаем системный прокси (ProxySettings := TProxySettings.Create('http://direct')), иначе запрос к API на localhost пойдет через сам sing-box.

Всё готово. Пошел хвастаться в китайский Telegram-чат по sing-box. Первый же доброволец написал, что у него ничего не работает.
Версия 0.2: JSONC
Первая версия почти ни у кого не работала из-за того, что я читал конфиг sing-box через TJSONObject, который не поддерживает лишние запятые и комментарии. Нужно было реализовать поддержку JSONC. Искал готовые библиотеки, не нашел ничего подходящего (чтобы дешево и сердито). Решил, что проще будет прочитать файл в строку, пробежаться по строке и удалить из нее все комментарии и лишние запятые (но для этого придется еще определять начало и конец литералов, потому что запятые могут быть и внутри JSON-строк), а потом передать нормализованный результат в обычный TJSONObject. Реализация оказалась простой.
Версия 0.3: быстрое переключение селекторов
Изначально для каждого селектора в меню трея было отдельное выпадающее подменю. Мне надоело водить мышкой по подменю для переключения серверов, ждать лишние доли секунды и иногда промазывать. Сделал альтернативный вариант отображения, когда все селекторы выводятся одним плоским списком с разделителями (хороший вариант, когда серверов мало). Показал в том же Telegram-чате. Некоторые пользователи снова посмеялись над моей программой, увидев выпадающее меню на весь экран с бесконечной прокруткой (оказывается, у многих в конфиге сотни серверов). Но всё равно оставил новый режим по умолчанию (позже переделаю).

Версия 0.4: TUN, управление процессом по-взрослому
Я все-таки решил добавить режим TUN, но пришлось многое переделать. Сам sing-box поддерживает TUN из коробки, достаточно добавить inbound tun в конфиг. Проблема в том, что TUN будет подниматься сразу при запуске, а еще для этого требуются права администратора. Прав нет — ничего не запускается. А еще мне нужен переключатель для пользователя. Я решил сделать так: пользователь сам добавляет inbound tun в конфиг, а программа при необходимости оттуда его удаляет и потом возвращает на место. При запуске ядра мы подсовываем ему нужную версию конфига, используя отдельные файлы для разных случаев.
К счастью, sing-box умеет читать конфиг не из файла, а из stdin (стандартный поток ввода). Мы сначала читаем конфиг, ищем в нем настройки TUN, вырезаем их, рисуем переключатель TUN, запускаем ядро с обрезанным конфигом. При нажатии кнопки TUN мы убиваем старое ядро и запускаем его заново — с правами администратора и расширенным конфигом.
Это всё потребовало более грамотного управления ядром (в том числе правильного завершения, а не просто убийства, чтобы ядро могло почистить за собой временные сетевые интерфейсы). Основная проблема была в том, что ядро может плавно завершить работу только при получении сигнала Ctrl+C, а этот сигнал невозможно отправить, если мы запускаем ядро без консоли (иначе будет черное окошко). Но есть вариант запустить с консолью и при этом с SW_HIDE, а потом перед отправкой сигнала подключиться к консоли через AttachConsole (GenerateConsoleCtrlEvent работает только в пределах своей консоли):
function TCoreSupervisor.SendCtrlCToConsole(processId: DWORD): boolean; begin FreeConsole; if not AttachConsole(processId) then exit(false); try SetConsoleCtrlHandler(nil, true); try result := GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); sleep(10); finally SetConsoleCtrlHandler(nil, false); end; finally FreeConsole; end; end;
GenerateConsoleCtrlEvent рассылает Ctrl+C всем процессам, прикреплённым к консоли — после AttachConsole мы в их числе. Поэтому SetConsoleCtrlHandler(nil, true) на время отправки заставляет наш процесс игнорировать сигнал.
С этой задачей помогли LLM, но и они тупили, путались, вводили в заблуждение (иногда писали, что без мигания консоли сделать невозможно).
Реализация оказалась замороченной: много пограничных случаев, легко выстрелить себе в ногу. CoreSupervisor работает в отдельном потоке, получает задачи через TThreadedQueue, сообщает в основной поток о результатах через TThread.Queue, мониторит состояние ядра (не упало ли). Всё это происходит в одном потоке, поэтому проверка состояния не должна тормозить обработку команд.
Добавил проверку наличия прав администратора, перезапуск с повышением прав. А еще, так как мы теперь правим конфиг на лету, добавил автоматическую генерацию секции Clash API с рандомным ключом, если эта секция у пользователя отсутствует.
Доделал, отправил в знакомый вам чат — у многих ничего не работает (программа зависает, невозможно закрыть).
Версия 0.5: правильно пишем в stdin
Ошибку удалось найти быстро. Оказалось, что дело в размере конфига. Я запускал ядро замороженным (с CREATE_SUSPENDED), писал конфиг в stdin, делал ResumeThread. Если буфера для записи не хватало, то всё зависало, потому что ядро не читало поток из-за заморозки. Ядро запускается замороженным, чтобы успеть добавить процесс в JobObject до возможного порождения им дочерних процессов (чтобы они тоже попали в JobObject и закрывались вместе со всеми). Стал писать в stdin после ResumeThread, и у всех всё заработало.
Версия 0.6: ждем готовности API перед сбросом селекторов
При запуске ядра программа сбрасывает селекторы через API в значения по умолчанию. Проблема в том, что API может подниматься долго (особенно при автозапуске вместе с Windows). Мы отправляем запросы, они не проходят, селекторы отображаются неверно. Пришлось переделать работу с API. Раньше мы запускали новые потоки и не следили за результатом. Теперь работа с API переехала в CoreSupervisor, управляется командами, результат контролируется. При запуске ядра мы ждем готовности API (периодически делаем запрос /version) и только потом сбрасываем селекторы в начальное состояние.
Версия 0.7: автоматическое обновление конфигурации с сервера
Я раздал программу членам семьи. Трудность в том, что при перенастройке прокси-сервера нужно каждому передать новый конфиг и помочь его обновить. Нужно сделать автоматическое обновление с сервера. Где хранить ссылку, где хранить время обновления? В конфиге sing-box подходящих полей нет, а любые неизвестные поля приводят к падению. Их можно вырезать перед передачей конфига ядру, но добавление новых полей идет вразрез с первоначальной идеей: ядро и конфиг должны быть оригинальные. Можно хранить новую информацию в отдельном файле, но у этого решения тоже есть минусы. Решил не изобретать велосипед и реализовать поддержку файлов BPF.
BPF — это недокументированный локальный формат конфигов мобильных клиентов sing-box. По сути, это архив с JSON-конфигом и дополнительными полями. Формат бинарный, вручную в текстовом редакторе не отредактировать. Содержимое:
1 байт — тип сообщения
1 байт — версия
gzip-данные с именем профиля, типом конфига, JSON-конфигом
Для удаленных конфигов: URL, флаг автоматического обновления, интервал обновления, дата последнего обновления
Программа загружает новую версию по URL через минуту после запуска, а потом с указанным интервалом. У этого THTTPClient, наоборот, системный прокси не отключаем: если URL заблокирован, запрос пройдёт через sing-box. К сожалению, конфиг только обновляется автоматически, но не применяется. Для применения новой версии всё еще требуется перезагрузка программы.
Версия 0.8: автозапуск через планировщик
С самого начала в программе не было встроенной возможности автозапуска вместе с Windows — пора это исправить. Решил делать через планировщик Windows, чтобы программа запускалась как можно быстрее. Через простой автозапуск приходится иногда ждать по несколько минут (а доступ в интернет нужен как можно быстрее). Плюс через планировщик можно сразу запускать с правами администратора, чтобы включение TUN было в один клик без дополнительного запроса прав.
Работа с планировщиком идет через OleObject, с реализацией помогли LLM. Сначала создал задание вручную через «Планировщик задач», экспортировал в XML, а потом на его основе формировал задание программно. Задача в планировщике запускается с повышенными правами, и для её создания нужны такие же — поэтому при изменении автозапуска появляется запрос прав.
При тестировании изредка всплывала проблема, что значок программы не появляется (всё работает, а значка нет). Думал, что баг в самом TTrayIcon. Найти настоящую причину снова помогли LLM.
Процесс стартовал раньше, чем Explorer создавал окно Shell_TrayWnd. Первый Shell_NotifyIcon(NIM_ADD, ...) уходил в никуда и тихо возвращал ошибку (VCL результат не проверяет). Стандартный механизм восстановления в Windows для таких случаев — broadcast-сообщение TaskbarCreated. Explorer рассылает его окнам, когда панель задач создана. VCL внутри TTrayIcon слушает это сообщение и при его получении повторно вызывает Shell_NotifyIcon(NIM_ADD, ...). То есть в норме после прихода TaskbarCreated значок появился бы сам. Но не появлялся — мешала встроенная защита Windows: по умолчанию она блокирует приватные оконные сообщения, идущие от процесса с обычными правами пользователя в процесс, запущенный от администратора. Explorer работает с правами пользователя, наша программа — с правами администратора, поэтому broadcast от Explorer до нас не доходил, а значит, и до VCL внутри нашего процесса.
Лечится в конструкторе наследника TTrayIcon (handle внутреннего окна доступен через protected-свойство Data.Wnd):
constructor TElevatedTrayIcon.Create(AOwner: TComponent); var msgId: UINT; begin inherited Create(AOwner); msgId := RegisterWindowMessage('TaskbarCreated'); ChangeWindowMessageFilterEx(Data.Wnd, msgId, MSGFLT_ALLOW, nil); end;
После этого блокировка для конкретного сообщения снята, broadcast приходит штатно, и VCL автоматически делает NIM_ADD.
Версия 0.9: полировка
Пришло время исправить старые проблемы и причесать функциональность.
Сделал опциональное сохранение состояния селекторов при перезапуске (раньше всегда устанавливались значения по умолчанию). Можно вернуть старое поведение через конфиг программы.
Многие жаловались на очень длинное выпадающее меню из-за большого количества серверов (и переставали пользоваться программой, хотя всегда была возможность сменить режим отображения). Добавил автоматический выбор режима отображения селекторов. Flat (прежний режим по умолчанию) — плоское меню (меньше движений, когда серверов мало, но невозможно пользоваться, если их много). Nested — вложенные меню (хорошо, когда серверов много). Теперь Auto — режим по умолчанию: стиль зависит от количества пунктов.
Переключение селекторов стало работать понятнее и стабильнее. Выбранный пункт визуально замораживается до получения ответа от API. Появляется меньше ошибок при переключении, так как теперь задержка в несколько секунд при закрытии старых соединений не приводит к появлению оповещения.
В случае проблемы при запуске ядра (или при падении в процессе работы) в оповещение вставляется текст ошибки, полученный из вывода ядра. Ранее было только универсальное сообщение.

При обновлении BPF-конфига в заголовок User-Agent HTTP-запроса добавляется версия ядра, чтобы скрипт на сервере мог генерировать конфиг с учетом версии.
Результат
Получилась легкая и удобная программа, которая запускает sing-box в фоне и позволяет управлять им через значок в трее.
Значок отображает статус системного прокси и TUN
Клик по значку переключает системный прокси
Быстрое включение и выключение TUN
Контекстное меню для переключения селекторов
Поддержка профилей с автоматическим обновлением с сервера
Ранний автозапуск через планировщик при загрузке системы
Базовая функциональность готова. Тратить еще больше времени на доработки не хотелось бы, хотя уверен, что руки будут чесаться. Очевидные недостатки:
Нет переключения профилей
Нет списка соединений с возможностью их завершения
Нет окна с логом
Как пользоваться
Скачайте sing-box-drover со страницы последнего релиза.
Скачайте sing-box для Windows с официальной страницы релизов (архив с
windows-amd64в названии).Распакуйте оба архива в одну папку (рядом должны лежать
sing-box-drover.exeиsing-box.exe).Положите туда же свой sing-box-конфиг и пропишите путь к нему в
sb-config-fileini-файла утилиты.Запустите
sing-box-drover.exe— в трее появится значок.
Подробности в README репозитория.
P. S.
Еще меня иногда просят убрать уродскую зеленую цветную капусту и нарисовать нормальный значок. Читать такое обидно, потому что началось всё именно со значка, да и мне самому он нравится. В общем, цветная капуста остаётся.
Репозиторий sing-box-drover: https://github.com/hdrover/sing-box-drover
Комментарии (5)

AngelNet
06.05.2026 09:36Свои велосипеды это безусловно полезно для разминки ума. Но есть же и готовые. Буквально сегодня искал себе и наткнулся на это: GUI launcher for sing-box. Written in Go. https://github.com/Leadaxe/singbox-launcher

hdrover Автор
06.05.2026 09:36Упомянутая вами программа появилась позже моей.
Полных аналогов не существует. Тут все-таки конкретная ниша - управление чистым sing-box с чистым универсальным переносимым между различными устройствами конфигом. Все существующее - это клиенты с GUI, где конфиг частично накидывается "мышкой" (без полного понимания что именно в конце будет передано в ядро), что не покрывает 100% возможностей ядра.
Упомянутая вами программа - это результат вайб-кодинга, где часть просто не работает, потому что никто не проверял. Например, плавное завершение ядра в этой программе было заявлено с первой версии, но десятки строк кода просто не работали (LLM их написала, но они не работали, никто их не проверил). А спустя несколько месяцев автор (Leadaxe) создал issue в репозитории sing-box, что, оказывается, ядро плавно невозможно завершить (https://github.com/SagerNet/sing-box/issues/3806). Это само собой не так. Вариант плавного завершения в том числе описан в этой статье. Так что о singbox-launcher у меня мнение крайне негативное.

AngelNet
06.05.2026 09:36Я лично не пробовал ваш лаунчер, у меня самописный лаунчер, который мне сделал и дал знакомый. Вайб кодинг осуждаю, считаю что его место для однострочных поделок с одной простой функцией, а не для сложных комплексных проектов. Не знаю зачем вы упоминаете Throne, он совершенно не решает некоторые задачи, доступные с помощью ванильного СБ + конфиг. Например у меня десять инстансов, все висят на локалхосте на разных портах, мне так нужно и так удобно. 127.0.0.1:2080 - мой основной зарубежный инстанс. 2081 - второй, 2082 - третий и так далее по списку. Все они доступны ОДНОВРЕМЕННО (mixed-proxy) т.е. например спуфинг у меня работает на 2085 порту и идет через российский инстанс, а браузер переключается между инстансами дополнительным аддоном/свитчером (что кстати очень удобно). Так вот почему я зацепился за Throne, когда я полез искать как это реализовать в нем или некобокс подобный функционал оказалось что никак. Только запускать отдельные экземпляры программы с собственными конфигами (что не только звучит дико, но и наверняка неудобно).
В общем желаю вашему проекту не быть заброшенным и привлечь контрибуторов кода! Всегда проще и легче решить что-то имея несколько светлых умов трудящихся над задачей.
K0Jlya9
Непонятно какая задача решалась, чем не устроил например throne. Хз зачем но там тоже есть иконка в трее и в ней можно выбрать сервер и включить-выключить системный прокси или tun.
hdrover Автор
Спасибо за комментарий.
Хочется использовать чистое свежее ядро с оригинальным json-конфигом (который я использую на всех устройствах). Без угадывания, что в каком окошке на что влияет. Без зависимости от промежуточного звена в виде переусложненного клиента (со своими багами). Развернуто я об этом писал в предыдущей статье: https://habr.com/ru/articles/1018964/
Как раз про это меню написано в самом начале. Чтобы выключить (или потом включить) системный прокси в Nekobox (и его форке Throne) нужно кликнуть на значок, переместить курсор на "системный прокси", потом переместить курсор на "отключить", потом только клик. Вместо одного клика по значку. Да, решается хитрой раздельной маршрутизацией (чтобы переключать вообще было не нужно).
Мне не нравится то, как разрабатывается Throne. То одно ломается, то другое. А оригинальный Nekobox уже заброшен.