Files
mc-proxy/client/mcproxy/client.go
Kyle Isom 6dc3e18925 Add per-route details to status, move socket to /srv/mc-proxy/
mcproxyctl status now shows individual routes per listener with
hostname, backend, mode, and re-encrypt indicator. Proto, gRPC
server, client library, and CLI all updated.

Default gRPC socket path moved from /var/run/mc-proxy.sock to
/srv/mc-proxy/mc-proxy.sock to match the service data directory
convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:52:37 -07:00

332 lines
8.9 KiB
Go

// Package mcproxy provides a client for the mc-proxy gRPC admin API.
package mcproxy
import (
"context"
"fmt"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
pb "git.wntrmute.dev/kyle/mc-proxy/gen/mc_proxy/v1"
)
// Client provides access to the mc-proxy admin API.
type Client struct {
conn *grpc.ClientConn
admin pb.ProxyAdminServiceClient
health healthpb.HealthClient
}
// Dial connects to the mc-proxy admin API via Unix socket.
func Dial(socketPath string) (*Client, error) {
conn, err := grpc.NewClient("unix://"+socketPath,
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("connecting to %s: %w", socketPath, err)
}
return &Client{
conn: conn,
admin: pb.NewProxyAdminServiceClient(conn),
health: healthpb.NewHealthClient(conn),
}, nil
}
// Close closes the connection to the server.
func (c *Client) Close() error {
return c.conn.Close()
}
// L7Policy represents an HTTP-level blocking policy.
type L7Policy struct {
Type string // "block_user_agent" or "require_header"
Value string
}
// Route represents a hostname to backend mapping with mode and options.
type Route struct {
Hostname string
Backend string
Mode string // "l4" or "l7"
TLSCert string
TLSKey string
BackendTLS bool
SendProxyProtocol bool
L7Policies []L7Policy
}
// ListRoutes returns all routes for the given listener address.
func (c *Client) ListRoutes(ctx context.Context, listenerAddr string) ([]Route, error) {
resp, err := c.admin.ListRoutes(ctx, &pb.ListRoutesRequest{
ListenerAddr: listenerAddr,
})
if err != nil {
return nil, err
}
routes := make([]Route, len(resp.Routes))
for i, r := range resp.Routes {
routes[i] = Route{
Hostname: r.Hostname,
Backend: r.Backend,
Mode: r.Mode,
TLSCert: r.TlsCert,
TLSKey: r.TlsKey,
BackendTLS: r.BackendTls,
SendProxyProtocol: r.SendProxyProtocol,
}
}
return routes, nil
}
// AddRoute adds a route to the given listener.
func (c *Client) AddRoute(ctx context.Context, listenerAddr string, route Route) error {
_, err := c.admin.AddRoute(ctx, &pb.AddRouteRequest{
ListenerAddr: listenerAddr,
Route: &pb.Route{
Hostname: route.Hostname,
Backend: route.Backend,
Mode: route.Mode,
TlsCert: route.TLSCert,
TlsKey: route.TLSKey,
BackendTls: route.BackendTLS,
SendProxyProtocol: route.SendProxyProtocol,
},
})
return err
}
// RemoveRoute removes a route from the given listener.
func (c *Client) RemoveRoute(ctx context.Context, listenerAddr, hostname string) error {
_, err := c.admin.RemoveRoute(ctx, &pb.RemoveRouteRequest{
ListenerAddr: listenerAddr,
Hostname: hostname,
})
return err
}
// FirewallRuleType represents the type of firewall rule.
type FirewallRuleType string
const (
FirewallRuleIP FirewallRuleType = "ip"
FirewallRuleCIDR FirewallRuleType = "cidr"
FirewallRuleCountry FirewallRuleType = "country"
)
// FirewallRule represents a firewall block rule.
type FirewallRule struct {
Type FirewallRuleType
Value string
}
// GetFirewallRules returns all firewall rules.
func (c *Client) GetFirewallRules(ctx context.Context) ([]FirewallRule, error) {
resp, err := c.admin.GetFirewallRules(ctx, &pb.GetFirewallRulesRequest{})
if err != nil {
return nil, err
}
rules := make([]FirewallRule, len(resp.Rules))
for i, r := range resp.Rules {
rules[i] = FirewallRule{
Type: protoToRuleType(r.Type),
Value: r.Value,
}
}
return rules, nil
}
// AddFirewallRule adds a firewall rule.
func (c *Client) AddFirewallRule(ctx context.Context, ruleType FirewallRuleType, value string) error {
_, err := c.admin.AddFirewallRule(ctx, &pb.AddFirewallRuleRequest{
Rule: &pb.FirewallRule{
Type: ruleTypeToProto(ruleType),
Value: value,
},
})
return err
}
// RemoveFirewallRule removes a firewall rule.
func (c *Client) RemoveFirewallRule(ctx context.Context, ruleType FirewallRuleType, value string) error {
_, err := c.admin.RemoveFirewallRule(ctx, &pb.RemoveFirewallRuleRequest{
Rule: &pb.FirewallRule{
Type: ruleTypeToProto(ruleType),
Value: value,
},
})
return err
}
// RouteStatus contains status information for a single route.
type RouteStatus struct {
Hostname string
Backend string
Mode string // "l4" or "l7"
BackendTLS bool
SendProxyProtocol bool
}
// ListenerStatus contains status information for a single listener.
type ListenerStatus struct {
Addr string
RouteCount int
ActiveConnections int64
ProxyProtocol bool
MaxConnections int64
Routes []RouteStatus
}
// Status contains the server's current status.
type Status struct {
Version string
StartedAt time.Time
TotalConnections int64
Listeners []ListenerStatus
}
// GetStatus returns the server's current status.
func (c *Client) GetStatus(ctx context.Context) (*Status, error) {
resp, err := c.admin.GetStatus(ctx, &pb.GetStatusRequest{})
if err != nil {
return nil, err
}
status := &Status{
Version: resp.Version,
TotalConnections: resp.TotalConnections,
}
if resp.StartedAt != nil {
status.StartedAt = resp.StartedAt.AsTime()
}
status.Listeners = make([]ListenerStatus, len(resp.Listeners))
for i, ls := range resp.Listeners {
routes := make([]RouteStatus, len(ls.Routes))
for j, r := range ls.Routes {
routes[j] = RouteStatus{
Hostname: r.Hostname,
Backend: r.Backend,
Mode: r.Mode,
BackendTLS: r.BackendTls,
SendProxyProtocol: r.SendProxyProtocol,
}
}
status.Listeners[i] = ListenerStatus{
Addr: ls.Addr,
RouteCount: int(ls.RouteCount),
ActiveConnections: ls.ActiveConnections,
ProxyProtocol: ls.ProxyProtocol,
MaxConnections: ls.MaxConnections,
Routes: routes,
}
}
return status, nil
}
// SetListenerMaxConnections updates the per-listener connection limit.
// 0 means unlimited.
func (c *Client) SetListenerMaxConnections(ctx context.Context, listenerAddr string, maxConns int64) error {
_, err := c.admin.SetListenerMaxConnections(ctx, &pb.SetListenerMaxConnectionsRequest{
ListenerAddr: listenerAddr,
MaxConnections: maxConns,
})
return err
}
// ListL7Policies returns L7 policies for a route.
func (c *Client) ListL7Policies(ctx context.Context, listenerAddr, hostname string) ([]L7Policy, error) {
resp, err := c.admin.ListL7Policies(ctx, &pb.ListL7PoliciesRequest{
ListenerAddr: listenerAddr,
Hostname: hostname,
})
if err != nil {
return nil, err
}
policies := make([]L7Policy, len(resp.Policies))
for i, p := range resp.Policies {
policies[i] = L7Policy{Type: p.Type, Value: p.Value}
}
return policies, nil
}
// AddL7Policy adds an L7 policy to a route.
func (c *Client) AddL7Policy(ctx context.Context, listenerAddr, hostname string, policy L7Policy) error {
_, err := c.admin.AddL7Policy(ctx, &pb.AddL7PolicyRequest{
ListenerAddr: listenerAddr,
Hostname: hostname,
Policy: &pb.L7Policy{Type: policy.Type, Value: policy.Value},
})
return err
}
// RemoveL7Policy removes an L7 policy from a route.
func (c *Client) RemoveL7Policy(ctx context.Context, listenerAddr, hostname string, policy L7Policy) error {
_, err := c.admin.RemoveL7Policy(ctx, &pb.RemoveL7PolicyRequest{
ListenerAddr: listenerAddr,
Hostname: hostname,
Policy: &pb.L7Policy{Type: policy.Type, Value: policy.Value},
})
return err
}
// HealthStatus represents the health of the server.
type HealthStatus int
const (
HealthUnknown HealthStatus = 0
HealthServing HealthStatus = 1
HealthNotServing HealthStatus = 2
)
func (h HealthStatus) String() string {
switch h {
case HealthServing:
return "SERVING"
case HealthNotServing:
return "NOT_SERVING"
default:
return "UNKNOWN"
}
}
// CheckHealth checks the health of the server.
func (c *Client) CheckHealth(ctx context.Context) (HealthStatus, error) {
resp, err := c.health.Check(ctx, &healthpb.HealthCheckRequest{})
if err != nil {
return HealthUnknown, err
}
return HealthStatus(resp.Status), nil
}
func protoToRuleType(t pb.FirewallRuleType) FirewallRuleType {
switch t {
case pb.FirewallRuleType_FIREWALL_RULE_TYPE_IP:
return FirewallRuleIP
case pb.FirewallRuleType_FIREWALL_RULE_TYPE_CIDR:
return FirewallRuleCIDR
case pb.FirewallRuleType_FIREWALL_RULE_TYPE_COUNTRY:
return FirewallRuleCountry
default:
return ""
}
}
func ruleTypeToProto(t FirewallRuleType) pb.FirewallRuleType {
switch t {
case FirewallRuleIP:
return pb.FirewallRuleType_FIREWALL_RULE_TYPE_IP
case FirewallRuleCIDR:
return pb.FirewallRuleType_FIREWALL_RULE_TYPE_CIDR
case FirewallRuleCountry:
return pb.FirewallRuleType_FIREWALL_RULE_TYPE_COUNTRY
default:
return pb.FirewallRuleType_FIREWALL_RULE_TYPE_UNSPECIFIED
}
}