Wire Protocol¶
This is a reference for the plugin wire protocol. Plugins built with the Go SDK handle this automatically — this page is for authors implementing plugins in other languages.
Channels¶
Plugins use two communication channels:
Windows
The stdin/stdout channel is available on Windows. The Unix socket channel is supported on Linux and macOS, so request-response hooks require one of those systems.
| Channel | Format | Purpose |
|---|---|---|
| stdin/stdout | Line-delimited JSON | Lifecycle events, target pushes |
| Unix socket | Length-prefixed msgpack | Request-response hooks |
stdin/stdout — Lifecycle & Pushes¶
Prox → Plugin: configure¶
Sent once per bound route after the plugin starts, and again on config reload. For autostart plugins with no route bindings, a single configure is sent with an empty route_id and no match.
{
"method": "configure",
"params": {
"route_id": "gateway:0",
"match": { "domain": "*.**", "path": "/ws" }
}
}
Plugin → Prox: ready¶
Sent after configure to declare request-response capabilities. This instructs prox to connect to the plugin's Unix socket for hook calls.
{
"method": "ready",
"params": {
"socket": "/tmp/prox-p-12345.sock",
"hooks": ["on_request", "on_response", "on_connect", "on_disconnect", "on_usage"]
}
}
Valid hook names: on_request, on_response, on_connect, on_disconnect, on_usage. prox ignores names it does not know, so a plugin can declare a hook that an older prox lacks.
Plugin → Prox: set_targets¶
Pushes new targets to route balancers. Sent whenever the target pool changes.
By route ID:
{
"method": "set_targets",
"params": {
"route_id": "gateway:0",
"targets": ["10.0.1.1:3505", "10.0.1.2:3505"]
}
}
By action name — updates all routes using the given action:
{
"method": "set_targets",
"params": {
"action": "dynamic_proxy",
"targets": ["10.0.1.1:3505", "10.0.1.2:3505"]
}
}
Wildcard — updates all routes with balancers:
{
"method": "set_targets",
"params": {
"route_id": "*",
"targets": ["10.0.1.1:3505", "10.0.1.2:3505"]
}
}
Grouped mode — compatible with any targeting method above:
{
"method": "set_targets",
"params": {
"action": "dynamic_proxy",
"groups": {
"de": ["de-node1.internal:8080", "de-node2.internal:8080"],
"us": ["us-node1.internal:8080"]
}
}
}
Field rules:
| Field | Type | Description |
|---|---|---|
route_id |
string |
Target a specific route, or "*" for all routes |
action |
string |
Target all routes using this action name |
targets |
[]string |
Flat replacement list (not a diff). Previous targets are discarded. |
groups |
map[string][]string |
Keyed replacement map. Each key gets its own sub-balancer. |
- Use
targetsorgroups, not both in the same message. - Use
route_idoraction, not both.
Plugin → Prox: set_speed¶
Pushes speed limits to route connections. Supports the same targeting as set_targets.
{
"method": "set_speed",
"params": {
"route_id": "gateway:0",
"download_mbps": 50,
"upload_mbps": 10
}
}
| Field | Type | Description |
|---|---|---|
route_id |
string |
Target route, or "*" for all routes |
action |
string |
Target all routes using this action |
download_mbps |
number |
Download bandwidth cap in Mbps |
upload_mbps |
number |
Upload bandwidth cap in Mbps |
group_key |
string |
When set, updates the rate for active group buckets with this key |
Plugin → Prox: drop_sessions¶
Closes every connection and stream on this prox instance that carries one of the session keys, upstream side included. prox does not reply; keys with no live connections are ignored.
An HTTP/1.1 client sees its connection close. On HTTP/2 only the stream is reset; other streams on the same connection continue.
Unix Socket — Request-Response Hooks¶
The socket uses length-prefixed msgpack frames:
Frame Structure¶
Each frame is an Envelope containing a hook type and the hook-specific data:
| Hook | t |
Reply |
|---|---|---|
on_request |
1 |
Yes |
on_response |
2 |
Yes |
on_connect |
3 |
Yes |
on_disconnect |
4 |
No |
on_usage |
5 |
No |
prox sends a hook only to plugins that declared it in ready. Payloads gain keys over time: skip keys you do not know, and treat a missing key as its zero value.
on_request¶
Request payload:
| Field | msgpack key | Type |
|---|---|---|
| RouteID | r |
string |
| Method | m |
string |
| Path | p |
string |
| Query | q |
string |
| Domain | d |
string |
| Host | ho |
string |
| Proto | pr |
string |
| RemoteAddr | a |
string |
| ContentLength | cl |
int64 |
| Headers | h |
map[string]string |
| Body | bd |
bytes |
| MatchDomain | md |
string |
| MatchGlob | mg |
string |
| MatchPath | mp |
string |
| Vars | v |
map[string]string |
| Target | tg |
string |
Response payload:
| Field | msgpack key | Type |
|---|---|---|
| Allow | ok |
bool |
| Drop | dr |
bool |
| Fallback | fb |
bool |
| Status | s |
int |
| Body | b |
string |
| Headers | h |
map[string]string |
| SpeedLimit | sp |
object |
| CleanQuery | cq |
bool |
| RewritePath | rp |
string |
| Group | gr |
string |
| Target | tg |
string |
| Session | se |
string |
se tags the stream with a session key for on_usage, on_disconnect and drop_sessions. When several plugins return one, the last non-empty key wins.
SpeedLimit object (sp):
| Field | msgpack key | Type | Description |
|---|---|---|---|
| DownloadMbps | dl |
float64 |
Download bandwidth cap in Mbps |
| UploadMbps | ul |
float64 |
Upload bandwidth cap in Mbps |
| GroupKey | gk |
string |
Aggregate key — connections with the same key share a single bandwidth pool |
on_response¶
Request payload — a pair of request info and upstream response:
ResponsePair {
req: RequestInfo // same as on_request payload
resp: UpstreamResponse {
status: int // msgpack key: "s"
headers: map[string]string // msgpack key: "h"
}
}
Response payload:
| Field | msgpack key | Type |
|---|---|---|
| Status | s |
int (0 = no change) |
| Headers | h |
map[string]string (add/override) |
| Remove | rm |
[]string (headers to remove) |
on_connect¶
Request payload:
| Field | msgpack key | Type |
|---|---|---|
| RouteID | r |
string |
| Domain | d |
string |
| RemoteAddr | a |
string |
| MatchDomain | md |
string |
| MatchGlob | mg |
string |
Response payload:
| Field | msgpack key | Type |
|---|---|---|
| Allow | ok |
bool |
| Group | gr |
string |
| Target | tg |
string |
| Session | se |
string |
on_disconnect¶
Fire-and-forget — no response expected. prox writes the frame and does not wait for a reply.
Request payload:
| Field | msgpack key | Type |
|---|---|---|
| RouteID | r |
string |
| Target | tg |
string |
| RemoteAddr | a |
string |
| BytesRx | rx |
int64 |
| BytesTx | tx |
int64 |
| DurationMs | ms |
int64 |
| Session | se |
string (empty when the stream had no key) |
on_usage¶
Fire-and-forget, sent every usage_interval (default 15s). Each entry holds the bytes one session moved on one route since the previous report. See Sessions and usage for what is counted.
Request payload:
| Field | msgpack key | Type |
|---|---|---|
| TimestampMs | ts |
int64 (Unix milliseconds) |
| Sessions | u |
[]SessionUsage |
SessionUsage:
| Field | msgpack key | Type |
|---|---|---|
| RouteID | r |
string |
| Session | se |
string |
| BytesRx | rx |
int64 |
| BytesTx | tx |
int64 |
A large report is split across several frames with the same timestamp.
Minimal Plugin Example (Bash)¶
#!/bin/bash
while IFS= read -r line; do
method=$(echo "$line" | jq -r '.method')
if [ "$method" = "configure" ]; then
route_id=$(echo "$line" | jq -r '.params.route_id')
echo '{"result":"ok"}'
# Push initial targets
echo "{\"method\":\"set_targets\",\"params\":{\"route_id\":\"$route_id\",\"targets\":[\"10.0.1.1:3505\"]}}"
fi
done
Note
Bash plugins support push-based target discovery only. Request-response hooks require a Unix socket server with msgpack framing — use the Go SDK or implement the socket protocol directly.