Go SDK¶
The Go SDK (github.com/dortanes/prox/sdk) provides a callback-based API for building plugins. It handles all transport details — stdin/stdout JSON messaging, Unix socket msgpack framing, and the ready handshake.
Windows
OnConfigure and push APIs can run over stdin/stdout on Windows. OnRequest, OnResponse, OnConnect, and OnDisconnect require Unix domain sockets and are supported on Linux and macOS.
Installation¶
Quick Start¶
Auth Plugin¶
package main
import "github.com/dortanes/prox/sdk"
func main() {
p := sdk.New()
p.OnRequest(func(req *sdk.Request) *sdk.Response {
token := req.Header("Authorization")
if token == "" {
return sdk.Deny(401, "Unauthorized")
}
return sdk.Allow(sdk.WithHeader("X-User-ID", "123"))
})
p.Run()
}
Target Discovery Plugin¶
package main
import "github.com/dortanes/prox/sdk"
func main() {
p := sdk.New()
p.OnConfigure(func(route sdk.Route) {
go func() {
targets := discoverTargets(route.Domain)
p.SetTargets(route.ID, targets)
}()
})
p.Run()
}
Hooks¶
OnConfigure(func(route Route))¶
Called when the plugin receives route configuration (on startup and reload).
Route fields:
| Field | Type | Description |
|---|---|---|
ID |
string |
Stable identifier (service:routeIndex) |
Domain |
string |
Domain pattern from config |
Path |
string |
Path pattern from config |
OnRequest(func(req *Request) *Response) — L7¶
Called for every HTTP request on the route. Returns a verdict:
sdk.Allow(opts...)— approve the request, optionally inject headers.sdk.Deny(status, body, opts...)— reject with an HTTP error response.sdk.Drop()— silently close the connection (no HTTP response).
p.OnRequest(func(req *sdk.Request) *sdk.Response {
// req.Method, req.Path, req.Domain, req.RemoteAddr
// req.Query, req.Host, req.Proto, req.ContentLength
// req.Header("Authorization"), req.Headers, req.Body
// req.QueryParam("token")
// req.MatchDomain, req.MatchGlob, req.MatchPath, req.Vars, req.Target
return sdk.Allow(sdk.WithHeader("X-Verified", "true"))
})
OnResponse(func(req *Request, resp *UpstreamResponse) *ResponseMod) — L7¶
Called after the upstream responds, before headers are sent to the client. Modify or remove headers, or override the status code.
p.OnResponse(func(req *sdk.Request, resp *sdk.UpstreamResponse) *sdk.ResponseMod {
return sdk.ModifyResponse(
sdk.WithResponseHeader("X-Frame-Options", "DENY"),
sdk.RemoveResponseHeader("Server"),
sdk.WithResponseStatus(200),
)
})
OnConnect(func(conn *ConnRequest) *ConnResponse) — L4¶
Called for raw TCP connections on pass routes (before TLS relay). Only SNI domain and remote address are available.
p.OnConnect(func(conn *sdk.ConnRequest) *sdk.ConnResponse {
if isBlacklisted(conn.RemoteAddr) {
return sdk.RejectConn()
}
return sdk.AcceptConn()
})
OnDisconnect(func(event *DisconnectEvent)) — Fire-and-Forget¶
Called after a connection ends (handler returns). Receives connection statistics. Unlike other hooks, this is fire-and-forget — no response is sent back to prox.
p.OnDisconnect(func(event *sdk.DisconnectEvent) {
log.Printf("route=%s target=%s bytes_rx=%d bytes_tx=%d duration=%dms",
event.RouteID, event.Target, event.BytesRx, event.BytesTx, event.DurationMs)
})
DisconnectEvent fields:
| Field | Type | Description |
|---|---|---|
RouteID |
string |
Route identifier (service:routeIndex) |
Target |
string |
Backend target host (SNI/domain) |
RemoteAddr |
string |
Client IP:port |
BytesRx |
int64 |
Bytes received from client (upload) |
BytesTx |
int64 |
Bytes transmitted to client (download) |
DurationMs |
int64 |
Connection duration in milliseconds |
Request Fields¶
| Field | Type | Description |
|---|---|---|
RouteID |
string |
Route identifier (service:routeIndex) |
Method |
string |
HTTP method (GET, POST, etc.) |
Path |
string |
Request path (/api/users) |
Query |
string |
Raw query string (foo=bar&baz=1) |
Domain |
string |
Request host without port |
Host |
string |
Full Host header including port |
Proto |
string |
HTTP protocol (HTTP/1.1, HTTP/2.0) |
RemoteAddr |
string |
Client IP:port |
ContentLength |
int64 |
Request body size (-1 if unknown) |
Headers |
map[string]string |
All request headers (first value only) |
Body |
[]byte |
Request body (capped at 64KB) |
MatchDomain |
string |
Captured * wildcard value(s) |
MatchGlob |
string |
Captured ** glob suffix |
MatchPath |
string |
Path pattern from config |
Vars |
map[string]string |
Route-level set variables |
Target |
string |
Backend target selected by balancer |
Helper Methods¶
| Method | Description |
|---|---|
req.Header(key) |
Returns the value of a request header |
req.QueryParam(key) |
Returns the value of a query parameter |
Response Helpers¶
Request Verdicts¶
sdk.Allow(opts...) // Approve request
sdk.Deny(status, body, opts...) // Reject with HTTP response
sdk.Fallback(opts...) // Route to the fallback action
sdk.Drop() // Silently close connection
sdk.WithHeader(key, value) // Inject header (allow: into request, deny: into response)
sdk.WithSpeedLimit(down, up) // Per-connection bandwidth cap (Mbps)
sdk.WithSpeedLimit(down, up, groupKey)// Grouped bandwidth cap (shared by connections with same key)
sdk.WithCleanQuery() // Remove query string from upstream request
sdk.WithRewritePath(path) // Override upstream request path
sdk.WithGroup(group) // Pick the balancer target from a named group
sdk.WithTarget(target) // Pin one exact target (wins over WithGroup)
Response Modifications¶
sdk.ModifyResponse(opts...) // Modify upstream response
sdk.NoResponseMod() // Pass through unchanged
sdk.WithResponseHeader(key, value) // Add or override response header
sdk.RemoveResponseHeader(key) // Remove response header
sdk.WithResponseStatus(status) // Override status code
L4 Connection Verdicts¶
sdk.AcceptConn() // Allow TCP connection
sdk.RejectConn() // Close TCP connection
sdk.WithConnGroup(group) // Pick the balancer target from a named group
sdk.WithConnTarget(target) // Pin one exact target (wins over WithConnGroup)
Push API¶
Target discovery and speed limiting use push methods over stdin/stdout (no socket required).
Target Updates¶
// By route ID:
p.SetTargets(routeID, []string{"10.0.1.1:8080", "10.0.1.2:8080"})
// By action name — updates all routes using the given action:
p.SetActionTargets("dynamic_proxy", []string{"10.0.1.1:8080", "10.0.1.2:8080"})
// Wildcard — updates all routes with balancers:
p.SetTargets("*", []string{"10.0.1.1:8080"})
Grouped Targets¶
p.SetGroupedTargets(routeID, map[string][]string{
"de": {"de-1:8080", "de-2:8080"},
"us": {"us-1:8080"},
})
p.SetActionGroupedTargets("dynamic_proxy", map[string][]string{
"de": {"de-1:8080", "de-2:8080"},
"us": {"us-1:8080"},
})
With domain pattern *.**, a request to de.example.com captures de — the balancer picks from the "de" group only. Each group gets its own sub-balancer using the route's strategy.
Per-Request Group Selection¶
sdk.WithGroup overrides the group for a single request, so the choice can come from anything the on_request hook sees — a header, a token, a database lookup — instead of the domain:
p.OnRequest(func(req *sdk.Request) *sdk.Response {
if tier := lookupTier(req.Header("Authorization")); tier != "" {
return sdk.Allow(sdk.WithGroup(tier))
}
return sdk.Allow()
})
The target reserved during route matching is released before the new one is picked, so leastconn counters stay accurate. If the named group has no available target and the route has no fallback, the request is routed to the action's fallback handler, or answered with 502 when none is configured. Routes without a balancer ignore the group and log a warning.
L4 pass routes work the same way through sdk.WithConnGroup, which picks the group for the whole TCP connection:
p.OnConnect(func(conn *sdk.ConnRequest) *sdk.ConnResponse {
return sdk.AcceptConn(sdk.WithConnGroup(regionFor(conn.RemoteAddr)))
})
Here the target is chosen after the hook returns, so nothing needs releasing. A group with no available target closes the connection — L4 has no fallback action.
Per-Request Target Pinning¶
sdk.WithGroup narrows the choice to a pool; sdk.WithTarget removes the choice entirely and names the upstream to use. It takes precedence over sdk.WithGroup, and the address may be any target the plugin has published — a member of any group, not only the one the domain resolves to:
p.SetGroupedTargets("*", map[string][]string{
"de": {"de-1:8080", "de-2:8080"},
"us": {"us-1:8080"},
})
p.OnRequest(func(req *sdk.Request) *sdk.Response {
if pinned := sessionServer(req.Header("Cookie")); pinned != "" {
return sdk.Allow(sdk.WithTarget(pinned)) // e.g. "de-2:8080"
}
return sdk.Allow(sdk.WithGroup("de"))
})
A pinned target that belongs to the balancer is reserved in the sub-pool that owns it, so leastconn counts it like any other request and releases it when the request finishes. A target outside every pool is still used — the plugin is trusted — but no strategy can account for it, so prox logs a warning. Either way the target picked during route matching is released first.
L4 pass routes use sdk.WithConnTarget with the same precedence and the same pool lookup:
p.OnConnect(func(conn *sdk.ConnRequest) *sdk.ConnResponse {
return sdk.AcceptConn(sdk.WithConnTarget("de-2:443"))
})
Routes whose upstream has no {target} placeholder ignore a pinned target, exactly as they ignore a group.
Speed Limiting¶
// By route ID:
p.SetSpeedLimit(routeID, sdk.SpeedLimit{DownloadMbps: 50, UploadMbps: 10})
// By action name:
p.SetActionSpeedLimit("proxy", sdk.SpeedLimit{DownloadMbps: 100})
// Wildcard — all routes:
p.SetSpeedLimit("*", sdk.SpeedLimit{DownloadMbps: 25})
When GroupKey is set, the push updates the rate for active group buckets with that key:
When multiple limits apply (config, push, response), the most restrictive value wins.
Method Reference¶
| Method | Description |
|---|---|
SetTargets(routeID, targets) |
Push flat targets for a specific route (or "*" for all) |
SetGroupedTargets(routeID, groups) |
Push grouped targets for a specific route (or "*" for all) |
SetActionTargets(action, targets) |
Push flat targets for all routes using the given action |
SetActionGroupedTargets(action, groups) |
Push grouped targets for all routes using the given action |
SetSpeedLimit(routeID, limit) |
Push speed limit for a specific route (or "*" for all) |
SetActionSpeedLimit(action, limit) |
Push speed limit for all routes using the given action |