Bastion

Proxying

Multi-protocol reverse proxy engine.

Bastion's proxy engine sits at the core of the gateway data plane. Every inbound request is matched to a route, forwarded to a healthy upstream target, and the response is streamed back to the client. The engine handles HTTP, WebSocket, SSE, and gRPC traffic with protocol-specific optimizations.

Protocol Support

Each route declares its protocol via the Protocol field. Bastion uses a dedicated handler per protocol type:

ProtocolConstantDescription
HTTP/HTTPSbastion.ProtocolHTTPStandard reverse proxy using Go's httputil.ReverseProxy
WebSocketbastion.ProtocolWebSocketFull-duplex WebSocket proxying with ping/pong keep-alive
SSEbastion.ProtocolSSEServer-Sent Events with streaming flush support
gRPCbastion.ProtocolGRPCgRPC proxying over HTTP/2
GraphQLbastion.ProtocolGraphQLHTTP-based GraphQL with schema-aware handling
bastion.WithRoute(bastion.RouteConfig{
    Path:     "/api/v1/*",
    Protocol: bastion.ProtocolHTTP,
    Targets:  []bastion.TargetConfig{{URL: "http://backend:8080", Weight: 1}},
    Enabled:  true,
})

HTTP Proxying

HTTP proxying is built on Go's httputil.ReverseProxy with a custom transport layer. The engine selects a target through the load balancer, rewrites the request path if configured, applies header policies, and forwards the request.

Connection Pooling

The proxy transport maintains a shared connection pool to upstream targets:

bastion.WithTimeouts(bastion.TimeoutConfig{
    Connect: 10 * time.Second,
    Read:    30 * time.Second,
    Write:   30 * time.Second,
    Idle:    90 * time.Second,
})
  • Connect -- maximum time to establish a TCP connection to the upstream.
  • Read -- maximum time to wait for the upstream response body.
  • Write -- maximum time to send the request body to the upstream.
  • Idle -- how long idle connections remain in the pool before being closed.

Buffer Pool

Request and response body sizes can be capped to protect gateway memory:

bastion.WithConfig(bastion.Config{
    BufferPool: bastion.BufferPoolConfig{
        MaxRequestBodySize:  10 * 1024 * 1024,  // 10 MB
        MaxResponseBodySize: 50 * 1024 * 1024,  // 50 MB
    },
})

WebSocket Proxying

When a route is configured with ProtocolWebSocket, the engine upgrades the client connection and establishes a mirrored connection to the upstream. Messages are relayed bidirectionally with configurable buffer sizes and keep-alive settings.

bastion.WithWebSocket(bastion.WebSocketConfig{
    ReadBufferSize:   4096,
    WriteBufferSize:  4096,
    HandshakeTimeout: 10 * time.Second,
    PingInterval:     30 * time.Second,
    PongTimeout:      60 * time.Second,
})
  • PingInterval -- how often the gateway sends WebSocket ping frames to both sides.
  • PongTimeout -- maximum time to wait for a pong response before closing the connection.

Active WebSocket connections are tracked in the gateway stats as ActiveWSConns.

SSE Proxying

Server-Sent Events require the proxy to stream the upstream response without buffering. The SSE handler sets Transfer-Encoding: chunked and flushes the response writer at a configurable interval.

bastion.WithSSE(bastion.SSEConfig{
    FlushInterval: 100 * time.Millisecond,
})

Active SSE connections are tracked as ActiveSSEConns in the gateway stats.

gRPC Proxying

Routes with ProtocolGRPC use HTTP/2 transport to forward gRPC frames. The proxy preserves gRPC trailers and status codes. TLS is typically required for gRPC upstreams -- see the TLS & mTLS subsystem.

Path Rewriting

The proxy engine supports three path manipulation modes, evaluated in order:

Strip Prefix

Remove the matched route prefix before forwarding:

bastion.WithRoute(bastion.RouteConfig{
    Path:        "/api/users/*",
    StripPrefix: true,
    Targets:     []bastion.TargetConfig{{URL: "http://users-svc:8080"}},
})
// Client: GET /api/users/123 -> Upstream: GET /123

Add Prefix

Prepend a path segment after stripping:

bastion.WithRoute(bastion.RouteConfig{
    Path:        "/v2/*",
    StripPrefix: true,
    AddPrefix:   "/api/v2",
    Targets:     []bastion.TargetConfig{{URL: "http://backend:8080"}},
})
// Client: GET /v2/items -> Upstream: GET /api/v2/items

Rewrite Path

Apply a regex-based path rewrite:

bastion.WithRoute(bastion.RouteConfig{
    Path:        "/old-api/*",
    RewritePath: "/new-api/$1",
    Targets:     []bastion.TargetConfig{{URL: "http://backend:8080"}},
})

Header Manipulation

Each route carries a HeaderPolicy with three operations applied to the upstream request:

bastion.WithRoute(bastion.RouteConfig{
    Path: "/api/*",
    Headers: bastion.HeaderPolicy{
        Add:    map[string]string{"X-Gateway": "bastion"},
        Set:    map[string]string{"Host": "internal.example.com"},
        Remove: []string{"X-Debug"},
    },
    Targets: []bastion.TargetConfig{{URL: "http://backend:8080"}},
})
  • Add -- appends headers (does not overwrite existing values).
  • Set -- sets headers (overwrites existing values).
  • Remove -- deletes headers from the request.

Additionally, when authentication is enabled and ForwardHeaders is true, the gateway injects X-Auth-Subject, X-Auth-Provider, X-Auth-Scopes, X-Auth-Role, and X-Auth-Email headers so upstream services receive the authenticated identity without re-authenticating.

Upstream Connection Management

The proxy engine integrates with several subsystems during request processing:

  1. Route matching -- the routing manager matches the request path and method to a route.
  2. Traffic splitting -- if a traffic policy is set, targets are filtered (canary, blue-green, A/B, mirror).
  3. Load balancing -- a healthy target is selected from the filtered set.
  4. Circuit breaker check -- if the target's circuit is open, the request is rejected with 503.
  5. Rate limiting -- global and per-route rate limits are enforced.
  6. Authentication -- credentials are validated if auth is enabled for the route.
  7. Caching -- cached responses are served without hitting the upstream.
  8. Proxy -- the request is forwarded and the response streamed back.
  9. Metrics and logging -- latency, status, and upstream info are recorded.

Graceful Shutdown

When the gateway shuts down, the proxy engine:

  1. Stops accepting new connections.
  2. Marks all targets as draining (SetDraining(true)), causing the load balancer to stop routing new requests to them.
  3. Waits for in-flight requests to complete (respecting the configured write timeout).
  4. Closes idle connections in the transport pool.
  5. Stops the health monitor and TLS reload loops.

On this page