S3 как транспортный протокол
идея и пререквизиты
вдохновленный изысканиями X в сфере данные удалены: XDRIVE transport, я решил попробовать самостоятельно реализовать какой-либо чудо-транспорт в условиях наших реалий. сразу оговорюсь, что необходимо рассматривать подобный "транспортный" протокол как резервный способ, когда нужно пробиться в интернет в услових максимальных ограничений, не используя существующие общеизвестные методы (WebRTC), так как они имеют тенденцию очень быстро терять актуальность.
таким образом, мой выбор сузился до двух вариантов: Яндекс Диск и S3 в Yandex Cloud. помиом того, что разбираться с API Яндекс Диска не было никакого желания, в голову закралась мысль, что потребительский продукт будет априори более сложным для кастомизации и, вероятнее всего, крайне медленным. поэтому для реализации чудо-транспорта мой выбор пал на S3. действительно, все оказалось очень несложно и идея сработала; PoC был готов буквально за час: клиент (Xray client) -> S3 -> VPS (Xray server) -> интернет. клиент и сервер используют один и тот же бакет на чтение и на запись.
для повторения эксперимента нам понадобится:
1) аккаунт в Yandex Cloud и стандартное хранилище S3 с доступом на чтение "с авторизацией".
не забываем создать сервсиный аккаунт с ролями storage.editor и выпустить к нему статичный ключ доступа. кладем креды в ~/.aws/credentials в соответствии с документацией:
[default]
aws_access_key_id = <идентификатор_статического_ключа>
aws_secret_access_key = <секретный_ключ>
2) сервер в свободном интернете с поднятым nginx, голым Xray с существующим инбаундом (в моем случае это VLESS-XHTTP-TLS). конечно, вы можете поднимать inbound с любымы протоколами, но я переиспользую существующий на моем сервере инбаунд, поэтому и клиенский конфиг в примере под XHTTP. голое ядро Xray в примере используется во-первых потому что я пользуюсь им сам, а во-вторых потому что мы не хотим самостоятельно писать поддержку TUN для данного PoC.
реализация туннеля и установка
сам туннель необходимо запускать и на клиенте и на VPS (не забывая при этом изменить параметр SERVER = True):
import socket
import threading
import time
from contextlib import closing
from itertools import count
import boto3
from botocore.config import Config
SERVER = False # change this param on free server
BUCKET = "bucket-name"
LISTEN = ("127.0.0.1", 8080)
TARGET = ("127.0.0.1", 443) # existing nginx HTTPS listener
S3 = boto3.client("S3", endpoint_url="https://storage.yandexcloud.net", region_name="ru-central1",
config=Config(request_checksum_calculation="when_required"))
tx, rx = ("s2c", "c2s") if SERVER else ("c2s", "s2c")
def get(key):
while True:
try:
with closing(S3.get_object(Bucket=BUCKET, Key=key)["Body"]) as body:
return body.read()
except S3.exceptions.NoSuchKey:
time.sleep(0.5) # polling rate
def send():
for n in count():
data = conn.recv(1024 * 1024) # 1mb chunks, needs tweaking
S3.put_object(Bucket=BUCKET, Key=f"S3-transport/{tx}/{n}", Body=data)
if not data: return
if SERVER:
get("S3-transport/c2s/0")
conn = socket.create_connection(TARGET)
else:
with socket.create_server(LISTEN) as listener:
print(f"Listening on {LISTEN}", flush=True)
conn, _ = listener.accept()
sender = threading.Thread(target=send, daemon=True)
sender.start()
for n in count():
key = f"S3-transport/{rx}/{n}"
data = get(key)
conn.sendall(data)
if not data: conn.shutdown(socket.SHUT_WR)
S3.delete_object(Bucket=BUCKET, Key=key)
if not data: break
sender.join()
conn.close()
не забываем установить boto3:
python3 -m venv .venv
.venv/bin/pip install boto3
запускаем туннель с помощью AWS_PROFILE=default .venv/bin/python tunnel.py. повторюсь, что туннель поднимается и на клиенте и на сервере, предварительно выставив праметр SERVER = True/False в зависимости от того, где запускается туннель.
клиентские настройки Xray
клиентский конфиг Xray по частям:
TUN:
{
"inbounds": [
{
"protocol": "tun",
"settings": {
"gateway": ["198.18.0.1/30", "fd00:198:18::1/126"],
"autoSystemRoutingTable": ["0.0.0.0/1", "128.0.0.0/1", "::/1", "8000::/1"]
},
"sniffing": {"enabled": true, "destOverride": ["tls"], "routeOnly": true}
},
{
"listen": "127.0.0.1",
"port": 1080,
"protocol": "socks",
"settings": {"udp": true}
}
],
настройки подключения до существующего xhttp inbound на сервере:
"outbounds": [
{
"tag": "S3",
"protocol": "vless",
"settings": {
"address": "127.0.0.1", "port": 8080,
"id": "user-id-00-00",
"encryption": "none"
},
"mux": {"enabled": true, "concurrency": -1, "xudpConcurrency": 1, "xudpProxyUDP443": "allow"},
"streamSettings": {
"network": "xhttp",
"security": "tls",
"tlsSettings": {
"serverName": "mysuperhiddenserver.com",
"alpn": ["h2"]
},
"xhttpSettings": {
"host": "mysuperhiddenserver.com",
"path": "/super-secret-xhttp-path",
"mode": "stream-one",
"extra": {"xmux": {"maxConnections": 1}}
}
}
},
{"tag": "direct", "protocol": "freedom"}
],
секция роутинга: прокидываем DNS и Yandex Cloud S3 во freedom, чтобы не гонять DNS через туннель и предотвратить зацикливание:
"routing": {
"rules": [
{"type": "field", "domain": ["full:storage.yandexcloud.net"], "outboundTag": "direct"},
{"type": "field", "port": "53", "outboundTag": "direct"}
]
}
}
ограничения и недостатки подхода
0) решение дорогое, медленное и не предназначено для ежедневного использования. я рассматриваю его только как экстренный вариант, когда надо подключиться и что-то посмотреть\отправить в мессенджере. если подумать, то это ограничение также является плюсом - такой метод вряд ли когда-либо станет мейнстримным. соответственно, можно ожидать, что он будет работать долго.
1) после каждого перезапуска туннеля необходимо вручную чистить директории S3-transport/c2s/ и S3-transport/s2c/.