Hoody Tunnel
Section titled “Hoody Tunnel”The hoody-tunnel kit provides multiplexed network tunnels between a Hoody container and external clients. A client opens a single WebSocket control connection and streams many TCP or UDP flows over it. The HTTP endpoints on this page expose the kit’s runtime state for inspection and let operators terminate sessions without holding the WebSocket themselves.
Use these endpoints to monitor active sessions, stream counts, binding inventory, and FD permit availability, to scrape Prometheus metrics, or to force-close a session with a graceful drain budget.
All endpoints live under the container-scoped tunnel host:
https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com
Health and metrics
Section titled “Health and metrics”GET /api/v1/tunnel/health
Section titled “GET /api/v1/tunnel/health”Standard kit health endpoint. No authentication required. Returns the kit’s status, service, build timestamp (built), start timestamp (started), process metadata (pid, fds, ip, userAgent), and a memory snapshot (memory).
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/healthimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.tunnel.kit.getHealth();Kit is healthy.
{ "status": "ok", "service": "hoody-tunnel", "started": "2024-01-15T12:00:00Z", "pid": 12345, "ip": "10.0.0.5", "userAgent": "hoody-tunnel/1.0", "built": "2024-01-15T10:00:00Z", "fds": 128, "memory": { "rss": 52428800, "heap": 33554432 }}GET /api/v1/tunnel/metrics
Section titled “GET /api/v1/tunnel/metrics”Returns Prometheus text-format metrics, including the number of active sessions, active bindings, and remaining FD permits.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/metricsimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.tunnel.kit.getMetrics();Prometheus text format.
# HELP hoody_tunnel_sessions_active Number of active tunnel sessions# TYPE hoody_tunnel_sessions_active gaugehoody_tunnel_sessions_active 5
# HELP hoody_tunnel_bindings_active Number of active bindings# TYPE hoody_tunnel_bindings_active gaugehoody_tunnel_bindings_active 12
# HELP hoody_tunnel_fd_permits_available FD permits available# TYPE hoody_tunnel_fd_permits_available gaugehoody_tunnel_fd_permits_available 488Active sessions and bindings
Section titled “Active sessions and bindings”GET /api/v1/tunnel/tunnels
Section titled “GET /api/v1/tunnel/tunnels”Returns a unified view of every active tunnel session, together with its EXPOSE and PULL bindings, stream counts, orphan count, and remaining FD budget. Use this endpoint for the most complete snapshot of tunnel state.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/tunnelsimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.tunnel.list();Tunnel overview.
{ "fdPermitsAvailable": 488, "orphanedSessions": 0, "totalBindings": 12, "totalStreams": 25, "sessions": [ { "sessionId": "sess_3f7a91c2e8b4d6f1", "peerAddr": "203.0.113.42:51234", "protocol": "hoody-tunnel.v2", "connectionsGranted": 10, "activeStreams": 3, "exposeBindings": [ { "bindId": 1, "containerPort": 8080 } ], "pullBindings": [ { "bindAddr": "127.0.0.1", "bindId": 2, "containerPort": 9000 } ] } ]}GET /api/v1/tunnel/sessions
Section titled “GET /api/v1/tunnel/sessions”Returns every active tunnel session with its bindings, stream counts, and protocol version.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/sessionsimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.tunnel.sessions.list();Active sessions.
{ "sessions": [ { "sessionId": "sess_3f7a91c2e8b4d6f1", "peerAddr": "203.0.113.42:51234", "isV2": true, "connectionsGranted": 10, "activeStreams": 3, "maxStreams": 100, "bindings": [ { "bindId": 1, "kind": "EXPOSE", "mode": "tcp", "containerPort": 8080 } ] } ], "total": 1}GET /api/v1/tunnel/bindings
Section titled “GET /api/v1/tunnel/bindings”Returns every active EXPOSE and PULL binding across all sessions, together with the owning session ID, port, kind, mode, and bind ID.
This endpoint takes no parameters.
curl https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/bindingsimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.tunnel.bindings.list();Active bindings.
{ "bindings": [ { "bindId": 1, "kind": "EXPOSE", "mode": "tcp", "port": 8080, "sessionId": "sess_3f7a91c2e8b4d6f1" }, { "bindAddr": "127.0.0.1", "bindId": 2, "kind": "PULL", "mode": "tcp", "port": 9000, "sessionId": "sess_3f7a91c2e8b4d6f1" } ], "total": 2}WebSocket control plane
Section titled “WebSocket control plane”GET /api/v1/tunnel/connect
Section titled “GET /api/v1/tunnel/connect”WebSocket upgrade endpoint for the multiplexed tunnel session. After the upgrade, the client sends exactly one binary frame as its first message: HELLO (0x01) to open a session or, on hoody-tunnel.v2 only, JOIN (0x09) with a single-use ticket from HELLO_OK to attach a secondary socket to an existing session.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
Sec-WebSocket-Protocol | header | string | Yes | Required. Comma-separated subprotocols to offer, in preference order: hoody-tunnel.v2, hoody-tunnel.v1, or hoody-tunnel.v2, hoody-tunnel.v1. |
curl -i -N \ -H "Connection: Upgrade" \ -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Sec-WebSocket-Version: 13" \ -H "Sec-WebSocket-Protocol: hoody-tunnel.v2" \ https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/connectThe WebSocket upgrade is accepted. A 101 Switching Protocols response carries no body.
Returned when the request omits the WebSocket subprotocol or names one the server does not recognize. The response advertises the accepted subprotocols in the x-hoody-tunnel-versions header.
Session termination
Section titled “Session termination”DELETE /api/v1/tunnel/sessions/{session_id}
Section titled “DELETE /api/v1/tunnel/sessions/{session_id}”Signals the tunnel kit to close the named session. Live sessions receive a GOAWAY(0x0001, "closed by admin") frame sent directly on the WebSocket and are force-closed after the drain budget elapses.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
session_id | path | string | Yes | Session ID as returned by GET /sessions |
grace_ms | query | integer | No | GOAWAY drain budget in ms (0-5000, default 50) |
curl -X DELETE \ "https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com/api/v1/tunnel/sessions/sess_3f7a91c2e8b4d6f1?grace_ms=100"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-tunnel-1.{serverName}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.tunnel.sessions.close('sess_3f7a91c2e8b4d6f1', { grace_ms: 100 });Close initiated.
{ "sessionId": "sess_3f7a91c2e8b4d6f1", "status": "close_initiated"}The grace_ms query value is outside the allowed range (0-5000).
{ "code": "INVALID_GRACE_MS", "error": "grace_ms must be <= 5000"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
INVALID_GRACE_MS | Invalid grace period | grace_ms is above the 5000 ms maximum. | Send a grace_ms from 0 to 5000, or leave it out for the default. |
No active session matches the supplied session_id.
{ "code": "SESSION_NOT_FOUND", "error": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
SESSION_NOT_FOUND | Session not found | No live or orphaned session has this ID. It may have closed already. | List the current sessions (GET /api/v1/tunnel/sessions) and use an ID from that list. |