v9: multi-destination tunnel support (parspack, mequ, alipi)

Server:
- /tunnel/<dest> routes: parspack (185.208.174.152:22),
  mequ (188.213.68.133:2022), alipi (10.66.66.2:22)
- /tunnel without dest defaults to parspack

Client (tunnel.py):
- --destination / -d flag to pick target
- Lists available destinations in --help

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-26 15:03:23 +04:00
parent 6aa2717560
commit 1d0b87b509
2 changed files with 75 additions and 32 deletions

34
chat.py
View File

@@ -25,8 +25,12 @@ import html
import urllib.parse
PORT = 9999
VERSION = "8"
TUNNEL_TARGET = ("185.208.174.152", 22)
VERSION = "9"
TUNNEL_TARGETS = {
"parspack": ("185.208.174.152", 22),
"mequ": ("188.213.68.133", 2022),
"alipi": ("10.66.66.2", 22),
}
MAX_FILE_SIZE = 1 * 1024 * 1024 # 1 MB per file
MAX_TOTAL_STORAGE = 50 * 1024 * 1024 # 50 MB total
@@ -380,10 +384,10 @@ def ws_make_frame(opcode, data):
return frame + data
async def handle_ws_tunnel(ws_reader, ws_writer):
"""Bridge WebSocket frames <-> raw TCP to TUNNEL_TARGET."""
async def handle_ws_tunnel(ws_reader, ws_writer, target):
"""Bridge WebSocket frames <-> raw TCP to target (host, port)."""
try:
ssh_reader, ssh_writer = await asyncio.open_connection(*TUNNEL_TARGET)
ssh_reader, ssh_writer = await asyncio.open_connection(*target)
except Exception as e:
ws_writer.write(ws_make_frame(0x8, struct.pack("!H", 1011) + str(e).encode()[:123]))
await ws_writer.drain()
@@ -569,8 +573,22 @@ async def handle_http(reader, writer, first_line):
writer.close()
return
# GET /tunnel — WebSocket upgrade for SSH tunnel
if method == "GET" and path == "/tunnel":
# GET /tunnel/<dest> — WebSocket upgrade for SSH tunnel
if method == "GET" and path.startswith("/tunnel"):
# Parse destination: /tunnel/parspack, /tunnel/mequ, or /tunnel (default: parspack)
parts = path.strip("/").split("/")
dest_name = parts[1] if len(parts) > 1 else "parspack"
target = TUNNEL_TARGETS.get(dest_name)
if not target:
names = ", ".join(TUNNEL_TARGETS.keys())
err = f"Unknown destination '{dest_name}'. Available: {names}".encode()
writer.write(b"HTTP/1.1 404 Not Found\r\n")
writer.write(f"Content-Length: {len(err)}\r\n".encode())
writer.write(b"\r\n")
writer.write(err)
await writer.drain()
writer.close()
return
ws_key = headers.get("sec-websocket-key", "")
if not ws_key:
writer.write(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
@@ -586,7 +604,7 @@ async def handle_http(reader, writer, first_line):
f"Sec-WebSocket-Accept: {accept}\r\n"
f"\r\n".encode())
await writer.drain()
await handle_ws_tunnel(reader, writer)
await handle_ws_tunnel(reader, writer, target)
return
# 404