# tcp-1.ssh.relay.ajam.dev — WebSocket ⇄ TCP relay

A stateless Cloudflare Worker that exposes raw TCP as a WebSocket, so an environment whose only
egress is HTTPS/443 can reach an SSH server — or any TCP service — on the public internet.
Bytes inside the WebSocket are the raw TCP stream: SSH stays end-to-end encrypted between your
client and the target, and this relay only forwards ciphertext.

This deployment: **1** (bucket `ap`, placement `aws:ap-south-1`).
Outbound TCP prefers the **Workers VPC network binding** (egress by Cloudflare Gateway, a less
shared address pool) and falls back to a direct Worker socket.

## Exact addresses

| What | Address |
| --- | --- |
| This document | https://tcp-1.ssh.relay.ajam.dev/ |
| Agent-readable copy | https://tcp-1.ssh.relay.ajam.dev/llms.txt |
| Machine-readable pool + ranking | https://tcp-1.ssh.relay.ajam.dev/relays.json |
| Diagnostics | https://tcp-1.ssh.relay.ajam.dev/trace |
| Liveness / this relay's colo + egress road stats | https://tcp-1.ssh.relay.ajam.dev/health |
| Client scripts (curl-able) | https://tcp-1.ssh.relay.ajam.dev/client/ws_ssh_relay.py, https://tcp-1.ssh.relay.ajam.dev/client/relay-rotate.py, https://tcp-1.ssh.relay.ajam.dev/client/ |
| Abuse contact | https://tcp-1.ssh.relay.ajam.dev/.well-known/security.txt |
| Default relay (WebSocket) | wss://tcp-1.ssh.relay.ajam.dev/ — → `railway.new:22` |
| Arbitrary target (WebSocket) | wss://tcp-1.ssh.relay.ajam.dev/connect/<host>/<port> — e.g. wss://tcp-1.ssh.relay.ajam.dev/connect/example.org/22 |
| Host-prefix form | wss://tcp-<name>.<this host>/ — selects named relay <name> when wildcard DNS exists |

Named relays:

- `wss://tcp-1.ssh.relay.ajam.dev/connect/railway` → `railway.new:22` (banner `SSH-2.0-Go`) — target bucket `us` — Railway free-VM SSH gateway (anonymous quota-gated)

## The colo pool — pick the relay nearest to you and the target

- `tcp.ssh.relay.ajam.dev` — client-nearest colo (no placement)
- `tcp-1.ssh.relay.ajam.dev` — bucket `ap`, placement `aws:ap-south-1` — South/East Asia and Oceania
- `tcp-2.ssh.relay.ajam.dev` — bucket `eu`, placement `aws:eu-central-1` — Europe, Middle East, Africa
- `tcp-3.ssh.relay.ajam.dev` — bucket `us`, placement `aws:us-east-1` — North and South America

`GET /relays.json` ranks the pool for **your** edge colo and the target, and accepts
`?prefer=client|target|auto`, `?region=<bucket>`, `?relay=<name>`, `?spread[=n]`
(rotate the front of the list to vary the egress colo), or `?host=&port=` for an explicit target.

Probe before committing if you care about latency (`time_starttransfer` shows the hop to the
relay's placement colo; every host shares the same anycast edge):

```sh
for h in tcp.ssh.relay.ajam.dev tcp-1.ssh.relay.ajam.dev tcp-2.ssh.relay.ajam.dev tcp-3.ssh.relay.ajam.dev; do
  curl -s -o /dev/null -w "%{time_starttransfer} $h\n" "https://$h/health"
done | sort -n | head -1
```

## How to use it — SSH (OpenSSH ProxyCommand)

Client: `ws_ssh_relay.py` (python3, standard library; it reads `$HTTPS_PROXY` when egress is
forced through an HTTP CONNECT proxy). A sandbox with only `curl` can fetch it from this relay:

```sh
curl -fsS https://tcp-1.ssh.relay.ajam.dev/client/ws_ssh_relay.py -o /tmp/ws_ssh_relay.py

# named target, this relay
ssh -o ProxyCommand='python3 /tmp/ws_ssh_relay.py wss://tcp-1.ssh.relay.ajam.dev/connect/railway' \
    -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=12 \
    railway.new 'uname -a; echo RELAY_OK'

# a specific colo from the pool
ssh -o ProxyCommand='python3 /tmp/ws_ssh_relay.py wss://tcp-1.ssh.relay.ajam.dev/connect/railway' \
    -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null railway.new 'uname -a'

# arbitrary target, IPv6 only
ssh -o ProxyCommand='python3 /tmp/ws_ssh_relay.py wss://tcp-1.ssh.relay.ajam.dev/connect/example.org/22?family=6' \
    -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null example.org
```

Pool failover in one command: `python3 relay-rotate.py <host1> <host2> <host3>` (served too).
Probe the hop without SSH: `python3 ws101.py tcp-1.ssh.relay.ajam.dev 443` → expect `HTTP/1.1 101 Switching Protocols`.

## Diagnostics — /trace

- `https://tcp-1.ssh.relay.ajam.dev/trace` — your edge colo/city/country/ASN, this relay's identity, egress-road counters.
- `https://tcp-1.ssh.relay.ajam.dev/trace?target=github.com:22&banner=1` — dials the target: address used, family, connect
  time, first banner line.
- `https://tcp-1.ssh.relay.ajam.dev/trace?egress=1` — dials an echo-IP service through the relay and prints the address the
  target sees (the relay's egress, not yours). `&path=vpc|direct` picks the road, `&format=text`
  prints `key=value` lines.

## Request knobs (connect)

| Knob | Meaning |
| --- | --- |
| `?family=4` / `?family=6` | resolve the target over DoH and dial only that address family (default `auto`) |
| `?path=vpc` / `?path=direct` | force the egress road for this session |
| `?dial=lazy` | dial on the first client byte instead of before the upgrade |
| `?token=<t>` / `/t/<t>/connect/...` / `X-Relay-Token` | token, when the operator set one |

## Protocol (for other clients)

1. `GET <address>` with `Upgrade: websocket`; no subprotocol; extension negotiation is the
   runtime's (a client that offers `permessage-deflate` gets it, one that offers nothing is
   uncompressed — the passthrough is byte-transparent either way).
2. After `101`, **binary** frames carry raw TCP bytes verbatim in both directions.
3. The TCP connection is dialed before the upgrade by default (timeout 10000 ms), so a
   dead target comes back as a plain HTTP `502`; `?dial=lazy` dials on the first client byte instead.
4. Half-close: an empty close frame stops client→target delivery and keeps the target's reply
   flowing until the target closes or a 15 s grace timer fires; the relay never closes the target
   socket just because the client closed its send side.
5. A zero-length keepalive frame every 25 s (clients ignore empty
   payloads) keeps edge and proxy WebSocket idle cuts from killing a quiet session.
6. Failures after `101` close the WebSocket with a reason; the `X-Relay-Session` header on the
   `101` identifies the session in the operator's logs.

## Limits and policy

- TCP only; no UDP, no inbound TCP, no HTTP forwarding (this is not a forward proxy).
- Platform-refused: port 25, Cloudflare IP ranges, localhost, private/link-local IPs.
- Guarded here: names that resolve into private space, internal-only suffixes, oversized frames,
  idle sessions (180000 ms), session caps (720 min /
  64 MiB).
- No token is required (PoC default). Any public target is allowed (PoC default).
- Cost: runs inside the Cloudflare Workers Paid $5/month plan; a session costs one request plus
  CPU per message (WebSocket messages are not billed as requests). No egress charge. Workers VPC
  is free during its open beta; Gateway logging applies to VPC egress.

## What this is not

Not a VPN, not authenticated beyond the token knob, not a general-purpose scanner. Treat it as a
temporary, monitored PoC endpoint; ask the operator before pointing it at anything sensitive.
