S3 as a transport protocol
idea and prerequisites
inspired by X's research into data deleted: XDRIVE transport, I decided to try implementing some kind of miracle transport myself, given our reality. let me say upfront that this kind of "transport" protocol should be treated as a fallback for getting through to the internet under the tightest restrictions, without using existing well-known methods (WebRTC), since those tend to stop being relevant very quickly.
so my choice came down to two options: Yandex Disk and S3 in Yandex Cloud. apart from having no desire to figure out the Yandex Disk API, I had a feeling that a consumer product would inherently be harder to customize and most likely extremely slow. so I went with S3 for my miracle transport. it really turned out to be very simple, and the idea worked; the PoC was ready in literally an hour: client (Xray client) -> S3 -> VPS (Xray server) -> internet. the client and server use the same bucket for both reads and writes.
to repeat the experiment, we'll need:
1) a Yandex Cloud account and standard S3 storage with read access set to "with authorization".
don't forget to create a service account with the storage.editor role and generate a static access key for it. put the creds in ~/.aws/credentials according to the docs:
[default]
aws_access_key_id = <static_key_id>
aws_secret_access_key = <secret_key>
2) a server on the unrestricted internet with nginx running and bare Xray with an existing inbound (VLESS-XHTTP-TLS in my case). of course, you can set up an inbound with any protocols, but I'm reusing the existing inbound on my server, so the client config in the example is for XHTTP too. i'm using the bare Xray core in this example first because I use it myself, and second because we don't want to write TUN support ourselves for this PoC.
tunnel implementation and setup
the tunnel itself needs to run on both the client and the VPS (don't forget to change the parameter to 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()
don't forget to install boto3:
python3 -m venv .venv
.venv/bin/pip install boto3
start the tunnel with AWS_PROFILE=default .venv/bin/python tunnel.py. again, the tunnel runs on both the client and the server; set SERVER = True/False beforehand depending on where you're running it.
Xray client settings
the Xray client config, piece by piece:
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}
}
],
settings for connecting to the existing xhttp inbound on the server:
"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"}
],
routing section: send DNS and Yandex Cloud S3 through freedom so we don't send DNS through the tunnel or end up with a loop:
"routing": {
"rules": [
{"type": "field", "domain": ["full:storage.yandexcloud.net"], "outboundTag": "direct"},
{"type": "field", "port": "53", "outboundTag": "direct"}
]
}
}
limitations and drawbacks of this approach
0) this is expensive, slow, and not meant for everyday use. i only see it as an emergency option when I need to connect and check something or send something in a messaging app. if you think about it, this limitation is also a plus: a method like this is unlikely to ever go mainstream. so we can expect it to keep working for a long time.
1) after every tunnel restart, you need to manually clear the S3-transport/c2s/ and S3-transport/s2c/ directories.