IPC Protocol
cmd/tunneld is the subprocess every non-iOS SDK adapter drives. It wraps
tunnelcat-server's Authenticator/TunnelDialer/SOCKS5Server behind a duplex,
newline-delimited JSON protocol on a local Unix domain socket. This page is the full contract — enough to write a
6th-language adapter without reading cmd/tunneld/main.go.
Transport
- Unix domain socket at a path you choose, passed via
tunneld -socket <path>. - Works identically on Linux, macOS, and Windows — Go's
net.Listen("unix", ...)is natively supported on Windows 10 1803+ viaAF_UNIX, so no separate named-pipe implementation exists or is needed. - One
tunneldprocess serves one tunnel session. Spawn a new process (with a new socket path) per session. - Framing: one JSON object per line (
\n-terminated), both directions.
Requests
{"id": <int>, "method": "<name>", "params": {...}}
id must be a positive integer you choose (increment per request); it's echoed back in the matching
response so you can correlate replies even if multiple requests are in flight. params is omitted for
methods that take none.
Responses
{"id": <int>, "result": {...}}
{"id": <int>, "error": "<message>"}
Exactly one of result / error is present.
Methods
Logs in (Authenticator.Login()) and starts a local SOCKS5 listener.
{"server": "https://control:443", "apikey": "", "username": "", "password": "", "socksAddr": "127.0.0.1:0", "pollingOnly": false}
socksAddr may be "127.0.0.1:0" (or omitted) to let the OS pick a port — read the
actual bound address back from the result. pollingOnly: true selects
NewTunnelDialerPolling instead of the default streaming NewTunnelDialer — required
against cmd/mockserver, since it only implements the polling side of the wire protocol (see
Wire Protocol).
Result: {"connected": true, "socksAddr": "127.0.0.1:54321", "state": "connected"}
No params. Tears down the SOCKS5 listener. Result: {"ok": true}.
No params. Result:
{"connected": bool, "socksAddr": "...", "uptimeSec": int, "state": "..."}.
No params. Re-runs Login() against the same credentials passed to the last
connect. Errors if not currently connected.
Events (unsolicited, pushed on every open connection)
{"event": "state", "state": "connecting"|"connected"|"disconnected"|"error", "error": "<message, only when state is error>"}
Pushed immediately when state changes, rather than requiring the client to poll the status method.
A client that only cares about request/response semantics can safely ignore any line containing an
"event" key.
Example session
A full connect → status → disconnect exchange, events interleaved as they actually arrive:
→ {"id":1,"method":"connect","params":{"server":"http://127.0.0.1:8443","apikey":"x","username":"u","password":"p","socksAddr":"127.0.0.1:1080","pollingOnly":true}}
← {"event":"state","state":"connecting"}
← {"event":"state","state":"connected"}
← {"id":1,"result":{"connected":true,"socksAddr":"127.0.0.1:1080","state":"connected"}}
→ {"id":2,"method":"status"}
← {"id":2,"result":{"connected":true,"socksAddr":"127.0.0.1:1080","state":"connected"}}
→ {"id":3,"method":"disconnect"}
← {"event":"state","state":"disconnected"}
← {"id":3,"result":{"ok":true}}
Every existing adapter follows the exact same shape: spawn tunneld -socket <random-temp-path>,
poll for the socket file to appear (or watch the process exit early as a failure signal), connect a Unix socket,
run a background reader thread/task that demuxes {"event":...} lines to your event callback and
{"id":...} lines to whichever call is waiting on that id, and write one JSON object per line for
each request. See any of the Python, Kotlin,
C#, or Swift sources for a complete reference implementation
of this pattern.