Не секрет, что в последнее время стабильность доступа к некоторым зарубежным ресурсам и API оставляет желать лучшего. Для обычного человека потеря связи с любимыми или нужными ресурсами посреди рабочего дня — это очень неприятно. Полагаться на один зарубежный сервер стало неэффективно: узлы падают, скорость проседает. У меня у самого появилась это проблема, поэтому я захотел сделать ее решение.
Что я решил сделать, рассказываю дальше.

Какая была задумка
Изначально, все что было в моих идеях — написать скрипт, который сохранял бы все доступные open‑source конфигурации из GitHub на мой компьютер и у меня появилась бы своя база данных конфигов, которые я могу copy‑paste в любой момент к себе на телефон и подключаться к ним.
Это была самая главная роль этого скрипта, ведь когда включают белые списки невозможно зайти в Google или GitHub и скопировать пару десятков‑сотен конфигураций к себе на телефон, а лезть в паблики ВК или Дзен, молясь найти героев, кто поделится своими — затея с успехом 10%.
В первый же день я написал первый прототип на Go, который каждый час скачивал новые конфигурации из репозиториев и сохранял их всех в.txt файл.
Какое улучшение мне предложили
Мой друг дал мне идею сделать из простого хранилища конфигураций, сервер с их раздачей по локальной сети на телефон, так мне просто было бы необходимо подключиться к той же сети, что и ПК, и клиент на телефоне сам автоматически обновит их. Так у меня были бы актуальные конфиги каждый день, ничего не делая.
Первое с чем я столкнулся — это конечно же как вообще работает раздача конфигураций. С помощью ИИ я узнал, что нужен обычный HTTP сервер, который при запросе отдает все конфиги в Body, каждый с новой строки, и прикрепляет особые заголовки к ответу:
profile-title— для названия подписки в клиентеprofile-update-interval— интервал автоматического обновления подписки в клиентеsubscription-userinfo— лимиты подпискиannounce— описание подпискиdate— текущая дата обновления подпискиrouting-enable— разрешена ли принудительная маршрутизация по правилам
Подробнее о всех них можете почитать в интернете или спросить у ИИ.

С какими проблемами я столкнулся
Самые очевидные проблемы, которые появились:
Утечка памяти
Data Race при записи и чтении файла конфигов
Нечитаемый код (А как могло быть иначе?)
Перегрузка клиента на телефоне из‑за тысяч конфигов
С утечкой памяти я довольно быстро разобрался, в основном, дело было в том, что я не закрывал Body после чтения новых конфигов. С Data Race мне пришлось немного поменять логику записи и чтения файлов, изначально я создал структуру, которая фактически должна была предотвращать Data Race ведь в ней был Mutex, но с файлом так не работает. Дело в том, что горутина чтения и записи файлов имели одну и ту же структуру с Mutex и *os.File по ссылке, когда одна писала в файл, она закрывала Mutex и не давала читающей горутине прочитать обрывки записанных данных, но я не учел, что в моментах между циклами записи новых конфигов были промежутки времени, когда Mutex был открыт и горутина чтения могла случайно прочитать обрывок недозаписанных данных.
Чтобы это решить я отказался от этой общей структуры и стал отдавать этим горутинам отдельные ссылки для управления файлом *os.File, но Data Race все равно никуда не пропадет, по этому я добавил логику с временным «temp» файлом. Когда новые конфиги скачиваются, они записываются во временный файл, а после, одной операцией файл переименовывается как главный. Переименовывание файла — это атомарная операция, по этому эти две горутины друг другу не мешают и горутина чтения никогда не прочитает обрывок данных.

После этого я разделил код в разные файлы и папки. Это мой обычный подход: сначала все пишешь как попало, а потом, когда понимаешь что потерял main функцию, начинаешь все чистить.
Оптимизация и улучшения
Что бы клиент на телефоне не перегружался тысячами конфигов, я решил сделать простую логику фильтрования. В путь запроса я теперь мог добавить сдвиг и ограничение количества конфигов, работает это так: в конец пути запроса вы можете добавить два числа через тире
.../sub/5-10и читается это как «начиная с 5 по счету конфига получить 10 штук» — благодаря такому простому решению теперь клиент не будет задыхаться при парсинге тысяч конфигов.После этого у меня был контроль над всеми конфигами, но была проблема — часть из них просто повторялась, так как в открытых репозиториях GitHub авторы могут брать одни и теже конфиги. Я сделал простой фильтр для уникальных конфигуриций с помощью
map, если по ключу вmapуже был такой же конфиг, его просто пропускали и шли дальше. С помощью этого я отсеивал 50% всех конфигов.
unique := make(map[string]struct{}) uniqueConfigs := make([]string, 0, 4000) for _, config := range configs { is, err := isUnique(config, unique) if err != nil { return nil, 0, fmt.Errorf("failed to check uniqueness for %s: %w", config, err) } if !is { copies++ continue } uniqueConfigs = append(uniqueConfigs, config) }
Не обязательно, что все конфиги все еще работают на момент их загрузки, по этому я добавил простой ping тест сервера, с помощью него я отсеивал еще 30% от оставшихся конфигов. Да, это не супер качественная проверка, но работает хорошо.
func isWorking(link string, timeout time.Duration) bool { conn, err := net.DialTimeout("tcp", link, timeout) if err != nil { return false } if conn != nil { defer conn.Close() return true } return false }
Авторы Open-Source конфигураций любят оставлять в названиях конфигов рекламу, длинные строки и другой мусор, по этому я написал логику форматирования названий конфигов по IP и стране. С помощью GeoLite я начал разделять конфигурации по странам и стандартизировал названия для всех конфигов, теперь они все формата:
🇰🇷 South Korea – #3646.
func formatName(parsedConfig *url.URL, name string, flag string, i int) { var builder strings.Builder builder.WriteString(flag) builder.WriteString(" ") builder.WriteString(name) builder.WriteString(" ") builder.WriteString("—") builder.WriteString(" ") builder.WriteString("#") builder.WriteString(strconv.Itoa(i + 1)) parsedConfig.Fragment = builder.String() }
Что бы оптимизировать получение конфигураций я добавил кеширование уже скаченных в оперативную память, просто с помощью структуры со списком и
Mutex. Теперь при каждом запросе на обновление конфигураций в клиенте не придется дергать файл.

Для удобства я добавил Web‑интерфейс, где есть QR‑код, чтобы не вводить ссылку в клиент вручную, управление фильтрацией, список конфигураций по странам, управление настройками подписки и скрипта (название, описание, порт, интервал и тд.), просмотр логов, принудительное обновление конфигураций и перезагрузка скрипта. Все это очень упрощает управление и радует глаза.

Недочеты и подводные камни
Я могу выделить несколько спорных решений и недочетов:
В чем смысл моего решения, если для него нужен ПК с Wi‑Fi, который может без ограничений заходить на GitHub? Думаю, что моя боль изначально была в том, что я забывал вручную заходить на GitHub и копировать новые конфигурации каждый день. Из‑за этого большинство из них на следующий день были уже нерабочими, а так клиент на телефоне сам сможет обновить конфигурации, если телефон будет подключен к той же сети, что и ПК.
Почему-то клиенты семейства V2Ray могут выдавать ошибку парсинга, но я понимаю, что это проблема самих клиентов. На других — все нормально.
Компьютер должен быть включен, иначе он не будет раздавать конфигурации. Это очевидно, но не приятно.
При частом принудительном обновлении конфигураций вам может прилететь Rate Limit от GitHub.
Заключение
В итоге за пару вечеров мне удалось собрать локальный агрегатор, который полностью закрывает мою потребность в актуальных конфигурациях и работает полностью в автоматическом режиме.
Весь исходный код проекта whitelist-download я выложил на GitHub. Проект открыт, лицензия позволяет свободно использовать и модифицировать код.
Буду очень рад конструктивной критике архитектуры, pull request'ам, issues и советам от опытных Go-разработчиков в комментариях.
