Делюсь способом генерации динамических ключей для подключения пользователей Outline vpn

Бесплатные 10 дней по ссылке в боте: artydevx@VPN
Большая проблема или забота о пользователях
Во времена блокировок некогда популярного 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: https://raw.githubusercontent.com/Jigsaw-Code/outline-server/master/src/shadowbox/server/api.yml
Далее напишем простенький веб-сервис, который будет отдавать пользовательские данные для подключения, в момент, когда клиент нажимает кнопку “Включить” в приложении 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 + сгенерированный сертификат
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...!
Результат запроса выглядит примерно так:
{ "server":"node1.outline.yourvpn.io", "server_port":"15200", "password":"Y2hhY2hhMjAtaWV", "method":"chacha20-ietf-poly1305" }
Преимущество этого метода в том, что вы, как создатель и администратор VPN сервиса можете без участия клиента изменить его точку подключения, не беспокоя его просьбами заменить конфиг файл.
Вдобавок вы можете балансировать награзку, если у вас достаточно пользователей, чтобы распределеять их по серверам.
Также оставляю пример собственной реализации динамических ключей в своем боте:
-> @exVPN <-

Не забудьте добавить поддержку CORS в ваше api-приложение, пример для fastapi
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
Если вам нужна помощь в автоматизации, работе с API или интеграции Outline в ваш продукт - пишите в телеграм artydevx
На пиво
Если данный материал оказался вам полезен - готов принять ваши копеечки :)
в крипте: ETH (ERC20): 0xcdc3231527a1ad105d527678ccbcf5e827747e7b
TON: UQAiIMLC2_j9tPlmQakdbz2Zh0rkTHH7tK2RTcO3rYAkr8QV
в рублях: https://pay.cloudtips.ru/p/2a3d8e06
Большое спасибо всем за внимание! Если вам интересны подобные рассуждения - подписывайтесь на мой канал artydev & Co.