Skip to content

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

go get github.com/dortanes/prox/sdk@v1.0.0

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:

p.SetSpeedLimit(routeID, sdk.SpeedLimit{DownloadMbps: 50, GroupKey: userID})

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