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/.