Documentation
¶
Overview ¶
Package relay — High Availability support for relay servers.
When the fleet exceeds ~100 nodes, a single relay server becomes a bottleneck and a single point of failure. HAProxy implements:
- Multiple relay server instances sharing a common store (Postgres)
- Consistent hashing to route nodes to preferred relay instances
- Automatic failover: if a relay goes down, its nodes reconnect to another
- Health monitoring between relay peers
- Graceful draining for zero-downtime upgrades
Architecture:
┌──────────────────────────────────┐
│ Load Balancer (L4) │
└──┬──────────┬──────────┬─────────┘
│ │ │
┌────▼──┐ ┌───▼───┐ ┌──▼─────┐
│Relay-1│ │Relay-2│ │Relay-3 │
└───┬───┘ └───┬───┘ └───┬────┘
│ │ │
┌───▼──────────▼──────────▼───┐
│ Shared Store (PG) │
└────────────────────────────-┘
Package relay — mTLS authentication for relay connections.
mTLS (mutual TLS) replaces shared bearer tokens with X.509 certificate-based authentication. Both the relay server and node agents present certificates signed by a shared CA, providing strong bidirectional identity verification.
Certificate hierarchy:
[CA cert] ─── signs ──► [Server cert] (relay server identity)
│
└── signs ──► [Node cert] (per-node identity, CN=<node-id>)
Usage:
# Generate CA, server, and node certs: devopsclaw relay cert-gen --ca --out /etc/devopsclaw/certs/ devopsclaw relay cert-gen --server --ca-cert ca.pem --ca-key ca-key.pem devopsclaw relay cert-gen --node --node-id web-01 --ca-cert ca.pem --ca-key ca-key.pem
Package relay provides NAT-traversal connectivity between the control plane and fleet nodes. Nodes make outbound connections to the relay server, eliminating the need for port forwarding, VPNs, or direct SSH access.
Architecture:
[Control Plane / CLI / Chat] ─── gRPC ───► [Relay Server] ◄─── gRPC ─── [Node Agent]
│ │
mTLS + auth outbound only
session mgmt auto-reconnect
multiplexing heartbeat
The relay server runs as a standalone service or embedded in the control plane. Node agents connect outbound (NAT-friendly) and maintain persistent streams. Commands are forwarded through the relay to target nodes.
Index ¶
- func ClientTLSConfig(cfg MTLSConfig) (*tls.Config, error)
- func ExtractNodeIDFromCert(state *tls.ConnectionState) (string, error)
- func GenerateCA(org string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
- func GenerateNodeCert(caCertPEM, caKeyPEM []byte, nodeID string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
- func GenerateServerCert(caCertPEM, caKeyPEM []byte, hosts []string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
- func ServerTLSConfig(cfg MTLSConfig) (*tls.Config, error)
- func WriteCertFiles(certPath, keyPath string, certPEM, keyPEM []byte) error
- type Agent
- type AgentConfig
- type ClientIdentity
- type ClusterState
- type CommandEnvelope
- type HAConfig
- type HACoordinator
- type LocalExecutor
- type MTLSConfig
- type PeerState
- type ResultEnvelope
- type Server
- func (s *Server) ConnectedNodes() []fleet.NodeID
- func (s *Server) DeregisterTunnel(nodeID fleet.NodeID)
- func (s *Server) RegisterTunnel(nodeID fleet.NodeID, remoteAddr string) (*Tunnel, error)
- func (s *Server) SendCommand(ctx context.Context, nodeID fleet.NodeID, env *CommandEnvelope) (*ResultEnvelope, error)
- type ServerConfig
- type ShellExecutor
- type Tunnel
- type TunnelRelayClient
- type WSAgent
- type WSMessage
- type WSRelayClient
- type WSServer
- type WSTunnel
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ClientTLSConfig ¶
func ClientTLSConfig(cfg MTLSConfig) (*tls.Config, error)
ClientTLSConfig builds a *tls.Config for a node agent connecting to the relay. The agent presents its own cert and verifies the server's cert against the CA.
func ExtractNodeIDFromCert ¶
func ExtractNodeIDFromCert(state *tls.ConnectionState) (string, error)
ExtractNodeIDFromCert extracts the node ID from a verified client certificate. The node ID is stored in the certificate's Common Name (CN) field.
func GenerateCA ¶
GenerateCA creates a self-signed CA certificate and private key.
func GenerateNodeCert ¶
func GenerateNodeCert(caCertPEM, caKeyPEM []byte, nodeID string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
GenerateNodeCert creates a client certificate for a fleet node, signed by the CA. The nodeID is embedded as the certificate's Common Name.
func GenerateServerCert ¶
func GenerateServerCert(caCertPEM, caKeyPEM []byte, hosts []string, validFor time.Duration) (certPEM, keyPEM []byte, err error)
GenerateServerCert creates a server certificate signed by the given CA.
func ServerTLSConfig ¶
func ServerTLSConfig(cfg MTLSConfig) (*tls.Config, error)
ServerTLSConfig builds a *tls.Config for the relay server with mTLS. The server presents its own cert and requires clients to present a CA-signed cert.
func WriteCertFiles ¶
WriteCertFiles writes cert and key PEM files to disk.
Types ¶
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent runs on each fleet node, maintaining an outbound connection to the relay.
func NewAgent ¶
func NewAgent(config AgentConfig, executor LocalExecutor, logger *slog.Logger) *Agent
NewAgent creates a node relay agent.
func (*Agent) IsConnected ¶
IsConnected returns whether the agent has an active relay connection.
type AgentConfig ¶
type AgentConfig struct {
RelayAddr string `json:"relay_addr"` // e.g., "relay.example.com:9443"
NodeID fleet.NodeID `json:"node_id"`
AuthToken string `json:"auth_token"` // legacy, prefer mTLS
TLSConfig *tls.Config `json:"tls_config,omitempty"`
MTLS *MTLSConfig `json:"mtls,omitempty"` // mTLS config (replaces AuthToken)
ReconnectInterval time.Duration `json:"reconnect_interval"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
}
AgentConfig configures the node-side relay agent.
type ClientIdentity ¶
type ClientIdentity struct {
NodeID string `json:"node_id"`
Fingerprint string `json:"fingerprint"`
Organization string `json:"organization"`
ValidUntil time.Time `json:"valid_until"`
}
ClientIdentity holds verified identity from a client certificate.
func VerifyClientCert ¶
func VerifyClientCert(state *tls.ConnectionState) (*ClientIdentity, error)
VerifyClientCert checks that the client certificate is valid and extracts identity information. Used in the WebSocket handler.
type ClusterState ¶
type ClusterState struct {
Self *PeerState `json:"self"`
Peers []*PeerState `json:"peers"`
TotalNodes int `json:"total_nodes"`
TotalInstances int `json:"total_instances"`
HealthyInstances int `json:"healthy_instances"`
}
ClusterState represents the complete HA cluster state.
type CommandEnvelope ¶
type CommandEnvelope struct {
RequestID string
Command fleet.TypedCommand
Deadline time.Time
}
CommandEnvelope wraps a command with routing metadata.
type HAConfig ¶
type HAConfig struct {
Enabled bool `json:"enabled"`
InstanceID string `json:"instance_id"` // Unique ID for this relay instance
PeerAddrs []string `json:"peer_addrs"` // Addresses of peer relay instances
AdvertiseAddr string `json:"advertise_addr"` // Address this instance advertises to peers
HealthInterval time.Duration `json:"health_interval"` // How often to check peer health
DrainTimeout time.Duration `json:"drain_timeout"` // How long to wait for connections to drain
}
HAConfig configures the relay HA cluster.
type HACoordinator ¶
type HACoordinator struct {
// contains filtered or unexported fields
}
HACoordinator manages relay HA cluster coordination.
func NewHACoordinator ¶
func NewHACoordinator(config HAConfig, server *WSServer, logger *slog.Logger) *HACoordinator
NewHACoordinator creates a new HA coordinator for the relay server.
func (*HACoordinator) ClusterStatus ¶
func (ha *HACoordinator) ClusterStatus() *ClusterState
ClusterStatus returns the current HA cluster state.
func (*HACoordinator) PreferredInstance ¶
func (ha *HACoordinator) PreferredInstance(nodeID string) string
PreferredInstance returns the preferred relay instance for a given node ID using consistent hashing. This helps distribute nodes across relay instances.
func (*HACoordinator) ShouldAcceptNode ¶
func (ha *HACoordinator) ShouldAcceptNode(nodeID string) bool
ShouldAcceptNode returns true if this relay instance should accept the given node, based on consistent hashing.
type LocalExecutor ¶
type LocalExecutor interface {
Execute(ctx context.Context, cmd fleet.TypedCommand) (*fleet.NodeResult, error)
}
LocalExecutor executes commands locally on the fleet node.
type MTLSConfig ¶
type MTLSConfig struct {
// Server-side
CACertFile string `json:"ca_cert_file"` // Path to CA certificate (PEM)
ServerCertFile string `json:"server_cert_file"` // Path to server certificate (PEM)
ServerKeyFile string `json:"server_key_file"` // Path to server private key (PEM)
// Client-side (node agent)
ClientCertFile string `json:"client_cert_file"` // Path to node certificate (PEM)
ClientKeyFile string `json:"client_key_file"` // Path to node private key (PEM)
// Policy
RequireClientCert bool `json:"require_client_cert"` // Reject connections without valid client cert
AllowTokenFallback bool `json:"allow_token_fallback"` // Allow bearer token if no client cert (migration mode)
}
MTLSConfig configures mutual TLS for the relay.
type PeerState ¶
type PeerState struct {
InstanceID string `json:"instance_id"`
Address string `json:"address"`
Status string `json:"status"` // "healthy", "unhealthy", "draining"
ConnectedNodes int `json:"connected_nodes"`
LastSeen time.Time `json:"last_seen"`
LastError string `json:"last_error,omitempty"`
}
PeerState tracks the health of a relay peer.
type ResultEnvelope ¶
type ResultEnvelope struct {
RequestID string
Result fleet.NodeResult
}
ResultEnvelope wraps a result with routing metadata.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the relay server that brokers connections between the control plane and fleet nodes.
func (*Server) ConnectedNodes ¶
ConnectedNodes returns the list of currently connected node IDs.
func (*Server) DeregisterTunnel ¶
DeregisterTunnel removes a node's tunnel.
func (*Server) RegisterTunnel ¶
RegisterTunnel is called when a node agent connects.
func (*Server) SendCommand ¶
func (s *Server) SendCommand(ctx context.Context, nodeID fleet.NodeID, env *CommandEnvelope) (*ResultEnvelope, error)
SendCommand routes a command to a node through its tunnel.
type ServerConfig ¶
type ServerConfig struct {
ListenAddr string `json:"listen_addr"` // e.g., ":9443"
TLSConfig *tls.Config `json:"tls_config,omitempty"`
AuthToken string `json:"auth_token"` // shared secret for node auth (legacy, prefer mTLS)
MaxNodes int `json:"max_nodes"`
PingInterval time.Duration `json:"ping_interval"`
MTLS *MTLSConfig `json:"mtls,omitempty"` // mTLS config (replaces AuthToken)
}
ServerConfig configures the relay server.
type ShellExecutor ¶
ShellExecutor implements LocalExecutor by running shell commands locally.
func NewShellExecutor ¶
func NewShellExecutor(workDir string) *ShellExecutor
NewShellExecutor creates a local shell command executor.
func (*ShellExecutor) Execute ¶
func (e *ShellExecutor) Execute(ctx context.Context, cmd fleet.TypedCommand) (*fleet.NodeResult, error)
Execute runs a typed command locally on this node.
type Tunnel ¶
type Tunnel struct {
NodeID fleet.NodeID
ConnectedAt time.Time
LastPing time.Time
RemoteAddr string
CommandCh chan *CommandEnvelope // commands to send to node
ResultCh chan *ResultEnvelope // results from node
Done chan struct{}
}
Tunnel represents a persistent connection from a node to the relay.
type TunnelRelayClient ¶
type TunnelRelayClient struct {
// contains filtered or unexported fields
}
TunnelRelayClient routes commands through the relay server.
func NewTunnelRelayClient ¶
func NewTunnelRelayClient(server *Server, logger *slog.Logger) *TunnelRelayClient
NewTunnelRelayClient creates a relay-based fleet client.
func (*TunnelRelayClient) Execute ¶
func (c *TunnelRelayClient) Execute(ctx context.Context, node *fleet.Node, cmd fleet.TypedCommand) (*fleet.NodeResult, error)
Execute sends a command to a node through the relay tunnel.
type WSAgent ¶
type WSAgent struct {
// contains filtered or unexported fields
}
WSAgent runs on each fleet node, connecting outbound to the relay server.
func NewWSAgent ¶
func NewWSAgent(config AgentConfig, executor LocalExecutor, logger *slog.Logger) *WSAgent
NewWSAgent creates a WebSocket-based node agent.
func (*WSAgent) IsConnected ¶
IsConnected returns whether the agent has an active relay connection.
type WSMessage ¶
type WSMessage struct {
Type string `json:"type"` // "register", "command", "result", "ping", "pong", "error"
RequestID string `json:"request_id,omitempty"`
NodeID string `json:"node_id,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
Error string `json:"error,omitempty"`
Timestamp time.Time `json:"ts"`
}
WSMessage is the wire format for relay messages.
type WSRelayClient ¶
type WSRelayClient struct {
// contains filtered or unexported fields
}
WSRelayClient routes commands through the WebSocket relay server.
func NewWSRelayClient ¶
func NewWSRelayClient(server *WSServer, logger *slog.Logger) *WSRelayClient
NewWSRelayClient creates a relay client backed by the WS server.
func (*WSRelayClient) Execute ¶
func (c *WSRelayClient) Execute(ctx context.Context, node *fleet.Node, cmd fleet.TypedCommand) (*fleet.NodeResult, error)
Execute sends a command to a node through the relay tunnel.
type WSServer ¶
type WSServer struct {
// contains filtered or unexported fields
}
WSServer is a WebSocket-based relay server that brokers connections between the control plane (CLI/SDK) and fleet node agents. Nodes connect outbound via WSS — no inbound ports required on nodes.
func NewWSServer ¶
NewWSServer creates a WebSocket relay server.
func (*WSServer) ConnectedNodeIDs ¶
ConnectedNodeIDs returns the list of currently connected node IDs.
func (*WSServer) SendCommandWS ¶
func (s *WSServer) SendCommandWS(ctx context.Context, nodeID fleet.NodeID, env *CommandEnvelope) (*ResultEnvelope, error)
SendCommandWS sends a command to a node through its WebSocket tunnel.