Большая проблема или забота о пользователях

Во времена блокировок некогда популярного VPN протокола Wireguard слава перешла к другому, еще более упрощенному по способу подключения через протокол Shadowsocks, многие из вас знают о нем по приложению Outline.

Вот так просто скачиваем программу менеджера, копируем команду на установку с официального сайта, добавляем ваш сервер и вуаля, готово!

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

Пример статичного ключа (плохой вариант):

ss://Y2hhY2hhMjAtaWV_*_VkgwN2VR@12.225.105.60:15200/?outline=1

Для тех, кто не понимает о чем речь - вы просто не дошли до момента, когда ip вашего сервера попадает в блеклист или с ним случается любая другая проблема. И что вам приходится делать? - Правильно, делаем очередную массовую рассылку с просьбой замены строки подключения пользователя.

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

Пример правильного ключа (хороший вариант):

ssconf://users.outline.yourvpn.io/conf/qwerty1235e0x112dd2exz#Wow!

Делюсь опытом, бесплатно

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

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

Ниже очень упрощенный пример, если у вас уже есть бот - вы сможете адаптировать его под свою реализацию.

class OutlineBackend:

    def __init__(self, server: str = None):
        self.session = requests.Session()
        self.server = get_server(server)
        self.base_url = self.server["apiUrl"]

    def _post(self, path: str, data=None):
        url = f"{self.base_url}/{path}"
        response = self.session.request(
            "POST", url, verify=False, headers=headers, data=data
        )
        if response.status_code == 201:
            return response.json()

    def _put(self, path: str, data=None):
        url = f"{self.base_url}/{path}"
        json_data = json.dumps(data)
        response = self.session.put(url, verify=False, headers=headers, data=json_data)
        if response.status_code == 204:
            return

    def rename_key(self, key_id: Union[str, int], key_name: str):
        return self._put(f"access-keys/{key_id}/name", data={"name": key_name})

    def create_new_key(self, name: str):
        response = self._post("access-keys")
        key_id = response.get("id")
        self.rename_key(key_id, name)
        return key_id

Результатом функции create_new_key(telegram_id) будет созданный новый ключ, который мы можем привязать к определенному пользователю и сохранить данные, например в таблице PostgreSQL.

Пример сохраненных данных с привязкой к telegram_id:

{
    "id": "telegram_id",
    "key_id": "1",
    "server": "node1.users.outline.yourvpn.io",
    "name": "key_telegram_id",
    "password": "GffvMo5Edsds2v",
    "server_port "24150",
    "method": "chacha20-ietf-poly1305",
    "access_url": "ss://Y2hhY2hhMjAtaWV_*_VkgwN2VR@12.225.105.60:15200/?outline=1"
}

Далее реализуем функцию генерации строки для динамического ключа.

OUTLINE_USERS_GATEWAY = "ssconf://users.outline.yourvpn.io"
OUTLINE_SALT = "qwerty123"
CONN_NAME = "Wow!"

def gen_outline_dynamic_link(user_id: Union[str, int]) -> str:
    user_id = int(user_id)
    return f"{OUTLINE_USERS_GATEWAY}/conf/{OUTLINE_SALT}{hex(user_id)}#{CONN_NAME}"

Результат сохраним в отдельной таблице, напомню, что функция должна возвращать следующую строку:

ssconf://users.outline.yourvpn.io/conf/qwerty1235e0x112dd2exz#Wow!

Как работает и для чего нужна динамическая ссылка можно почитать тут: https://www.reddit.com/r/outlinevpn/wiki/index/dynamic_access_keys/

Далее напишем простенький веб-сервис, который будет отдавать пользовательские данные для подключения, в момент, когда клиент нажимает кнопку “Включить” в приложении Outline на своем устройстве.

OUTLINE_SALT = "qwerty123"
CMD = "select server, server_port, password, method from schema.dt_outline_info where id = %s"

app = FastAPI()

def get_config_by_id(user_id: Union[str, int]) -> dict:
    conn = DataBaseConnector()
    user_id = str(user_id)
    result = dict(conn.select(CMD, (user_id, ))[0])
    return result


@app.get('/conf/%s{hex_id}' % OUTLINE_SALT)
async def handle_payment(hex_id: str):
    user_id = int(hex_id, 0)
    response = get_config_by_id(user_id)
    return response

Запустим его через systemd

[Unit]

Description=gunicorn daemon
After=network.target

[Service]
User=root
Group=www-data
WorkingDirectory=/home/vpn/outline-gateway
Environment="PATH=/home/vpn/outline-gateway/env/bin"
ExecStart=/home/vpn/outline-gateway/env/bin/gunicorn -w 1 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:5000 main:app

[Install]
WantedBy=multi-user.target

И проксируем через nginx + сгенерированный сертификат через certbot

server {
        server_name users.outline.yourvpn.io;
        location / {
                include proxy_params;
                proxy_pass http://127.0.0.1:5000;
        }
}

Заключение

Что мы имеем по итогу? Пользователь получает ссылку вида:

ssconf://users.outline.yourvpn.io/conf/qwerty1235e0x112dd2exz#Wow!

Результат HTTP-запроса (результата) можно посмотреть заменив ssconf на https, получив ссылку вида:

https://users.outline.yourvpn.io/conf/qwerty1235e0x112dd2exz#Wow!

Результат запроса выглядит примерно так:

{
    "server":"node1.outline.yourvpn.io",
    "server_port":"15200",
    "password":"Y2hhY2hhMjAtaWV",
    "method":"chacha20-ietf-poly1305"
}

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

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


Большое спасибо всем за внимание! Если вам интересны подобные рассуждения - подписывайтесь на мой канал artydev & Co.

Комментарии (9)


  1. anzay911
    12.09.2023 03:36

    Это в смысле подписка? У меня Android клиенты не понимают, что означает ssconf://. Или нужно на сервере преобразовывать в https?


    1. artydev Автор
      12.09.2023 03:36
      +1

      Вы имеете ввиду клиенты shadowsocks?
      ssconf - конфиг принимает Outline, на счет остальных не уверен (не тестировал)
      В них наверное можно попробовать отдать https


  1. karrakoliko
    12.09.2023 03:36
    +8

    ip вашего сервера попадает в блеклист или с ним случается любая другая проблема. И что вам приходится делать? - Правильно, делаем очередную массовую рассылку с просьбой замены строки подключения пользователя.

    ssconf://users.outline.yourvpn.io

    с доменом типа ничего не произойдёт, и ваш толковый бот совершенно точно не сломается, достучится до users.outline.yourvpn.io, и выдаст рабочий ключик пользователю, я правильно вас понял? :)

    у вас там в толковых телеграм каналах про массовые блокировки доменов и впн сервисов в россии ничего не слышали?


    1. artydev Автор
      12.09.2023 03:36

      А я и не утверждаю, что именно домен должен решить все существующие проблемы, идея домена как минимум в том, чтобы разделить сервис с конфигами и VPN - сервис.

      Слышали, но про блокировки сайтов - пока только проект, в котором упоминается "блокировка сайтов содержащих информацию о обходе и доступу ..."

      К блокировке поддомена, который принимает только GET запрос для узкого пула пользователей, пока не вижу никаких предпоссылок.


    1. heejew
      12.09.2023 03:36
      +2

      С доменом, будем честны, рисков меньше, да и проблем, в случае срабатывания рисков, тоже. ВПН сервисы детектятся по протоколу, и по пулам ip адресов, и именно до Outline пока что не дотянулись. По fqdn vpnы не лочат, а если на домене не хостить сайтик с запрещённой, то и словить блок домена будет как-то сложновато.

      Самое ключевое, что даже при обычном переезде сервак не требуется перекидывать новые ключики юзерам. Что довольно таки полезно

      Понятно, что это все текущие реалии и со временем что-то может сильно измениться. Ну когда изменится, тогда и посмотрим, все равно не угадаешь заранее, где выстрелит, и нет совсем безпроигрышных решений. А на сейчас сойдёт.


    1. FelixTheMagnificent
      12.09.2023 03:36
      +1

      Имхо, этот ключик можно захостить и в S3 bucket.
      Никто даже не заметит этот текстовый файл.


  1. heejew
    12.09.2023 03:36

    Спасибо! На самом деле не знал, что у outline такая возможность есть. ( да я и не искал ))

    Но фича таки полезная


  1. Negash
    12.09.2023 03:36

    Outline масштабироваться то научился или нет?


    1. artydev Автор
      12.09.2023 03:36

      Из коробки - нет, только самому распределять пользователей по нодам.
      Как один из вариантов - через динамический ключ