Как я создал локальное хранилище и агрегатор VLESS конфигураций

от автора

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

Что я решил сделать, рассказываю дальше.

Какая была задумка

Изначально, все что было в моих идеях — написать скрипт, который сохранял бы все доступные open‑source конфигурации из GitHub на мой компьютер и у меня появилась бы своя база данных конфигов, которые я могу copy‑paste в любой момент к себе на телефон и подключаться к ним.

Это была самая главная роль этого скрипта, ведь когда включают белые списки невозможно зайти в Google или GitHub и скопировать пару десятков‑сотен конфигураций к себе на телефон, а лезть в паблики ВК или Дзен, молясь найти героев, кто поделится своими — затея с успехом 10%.

В первый же день я написал первый прототип на Go, который каждый час скачивал новые конфигурации из репозиториев и сохранял их всех в.txt файл.

Какое улучшение мне предложили

Мой друг дал мне идею сделать из простого хранилища конфигураций, сервер с их раздачей по локальной сети на телефон, так мне просто было бы необходимо подключиться к той же сети, что и ПК, и клиент на телефоне сам автоматически обновит их. Так у меня были бы актуальные конфиги каждый день, ничего не делая.

Первое с чем я столкнулся — это конечно же как вообще работает раздача конфигураций. С помощью ИИ я узнал, что нужен обычный HTTP сервер, который при запросе отдает все конфиги в Body, каждый с новой строки, и прикрепляет особые заголовки к ответу:

  1. profile-title — для названия подписки в клиенте

  2. profile-update-interval — интервал автоматического обновления подписки в клиенте

  3. subscription-userinfo — лимиты подписки

  4. announce — описание подписки

  5. date — текущая дата обновления подписки

  6. 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.Builderbuilder.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‑код, чтобы не вводить ссылку в клиент вручную, управление фильтрацией, список конфигураций по странам, управление настройками подписки и скрипта (название, описание, порт, интервал и тд.), просмотр логов, принудительное обновление конфигураций и перезагрузка скрипта. Все это очень упрощает управление и радует глаза.

Недочеты и подводные камни

Я могу выделить несколько спорных решений и недочетов:

  1. В чем смысл моего решения, если для него нужен ПК с Wi‑Fi, который может без ограничений заходить на GitHub? Думаю, что моя боль изначально была в том, что я забывал вручную заходить на GitHub и копировать новые конфигурации каждый день. Из‑за этого большинство из них на следующий день были уже нерабочими, а так клиент на телефоне сам сможет обновить конфигурации, если телефон будет подключен к той же сети, что и ПК.

  2. Почему-то клиенты семейства V2Ray могут выдавать ошибку парсинга, но я понимаю, что это проблема самих клиентов. На других — все нормально.

  3. Компьютер должен быть включен, иначе он не будет раздавать конфигурации. Это очевидно, но не приятно.

  4. При частом принудительном обновлении конфигураций вам может прилететь Rate Limit от GitHub.

Заключение

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

Весь исходный код проекта whitelist-download я выложил на GitHub. Проект открыт, лицензия позволяет свободно использовать и модифицировать код.

Буду очень рад конструктивной критике архитектуры, pull request’ам, issues и советам от опытных Go-разработчиков в комментариях.

ссылка на оригинал статьи https://habr.com/ru/articles/1057636/