phase 4: connectrpc on the gateway authenticated edge

Replace the native-gRPC server bootstrap with a single
`connectrpc.com/connect` HTTP/h2c listener. Connect-Go natively
serves Connect, gRPC, and gRPC-Web on the same port, so browsers can
now reach the authenticated surface without giving up the gRPC
framing native and desktop clients may use later. The decorator
stack (envelope → session → payload-hash → signature →
freshness/replay → rate-limit → routing/push) is reused unchanged
behind a small Connect → gRPC adapter and a `grpc.ServerStream`
shim around `*connect.ServerStream`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-07 11:49:28 +02:00
parent 39b7b2ef29
commit 118f7c17a2
30 changed files with 1009 additions and 772 deletions
+61 -48
View File
@@ -1,4 +1,10 @@
// Package grpcapi exposes the authenticated gRPC surface of the gateway.
// Package grpcapi exposes the authenticated edge transport surface of the
// gateway. Despite the historical package name, the listener is built on
// `connectrpc.com/connect` and natively serves the Connect, gRPC, and
// gRPC-Web protocols on a single HTTP/h2c listener. The configured Go
// types and environment variable names retain the `gRPC` infix for
// operational stability — they describe the authenticated edge tier, not
// the wire protocol.
package grpcapi
import (
@@ -6,6 +12,7 @@ import (
"errors"
"fmt"
"net"
"net/http"
"sync"
"galaxy/gateway/authn"
@@ -18,14 +25,17 @@ import (
"galaxy/gateway/internal/session"
"galaxy/gateway/internal/telemetry"
gatewayv1 "galaxy/gateway/proto/galaxy/gateway/v1"
"galaxy/gateway/proto/galaxy/gateway/v1/gatewayv1connect"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"connectrpc.com/connect"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.uber.org/zap"
"google.golang.org/grpc"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// ServerDependencies describes the optional collaborators used by the
// authenticated gRPC server. The zero value is valid and keeps the process
// authenticated edge server. The zero value is valid and keeps the process
// runnable with the built-in unimplemented service stub.
type ServerDependencies struct {
// Service optionally handles the post-bootstrap SubscribeEvents lifecycle
@@ -45,12 +55,12 @@ type ServerDependencies struct {
ResponseSigner authn.ResponseSigner
// SessionCache resolves authenticated device sessions after the envelope
// gate succeeds. When nil, the authenticated gRPC surface remains runnable
// gate succeeds. When nil, the authenticated edge surface remains runnable
// but valid envelopes fail closed as session-cache unavailable.
SessionCache session.Cache
// Clock provides current server time for freshness checks. When nil, the
// authenticated gRPC surface uses the system clock.
// authenticated edge surface uses the system clock.
Clock clock.Clock
// ReplayStore reserves authenticated request identifiers after signature
@@ -59,26 +69,28 @@ type ServerDependencies struct {
ReplayStore replay.Store
// Limiter applies authenticated rate limits after the request passes the
// transport authenticity checks. When nil, the authenticated gRPC surface
// transport authenticity checks. When nil, the authenticated edge surface
// uses a process-local in-memory limiter.
Limiter AuthenticatedRequestLimiter
// Policy evaluates later authenticated edge policy after rate limits pass.
// When nil, the authenticated gRPC surface applies a no-op allow policy.
// When nil, the authenticated edge surface applies a no-op allow policy.
Policy AuthenticatedRequestPolicy
// Logger writes structured logs for authenticated gRPC traffic.
// Logger writes structured logs for authenticated edge traffic.
Logger *zap.Logger
// Telemetry records low-cardinality gRPC metrics.
// Telemetry records low-cardinality edge metrics.
Telemetry *telemetry.Runtime
// PushHub is the active authenticated push-stream hub. When present, the
// server closes active streams before GracefulStop during shutdown.
// server closes active streams before HTTP graceful shutdown.
PushHub *push.Hub
}
// Server owns the authenticated gRPC listener exposed by the gateway.
// Server owns the authenticated edge HTTP/h2c listener exposed by the
// gateway. It serves the Connect, gRPC, and gRPC-Web protocols from a
// single net/http listener.
type Server struct {
cfg config.AuthenticatedGRPCConfig
service gatewayv1.EdgeGatewayServer
@@ -87,11 +99,11 @@ type Server struct {
metrics *telemetry.Runtime
stateMu sync.RWMutex
server *grpc.Server
server *http.Server
listener net.Listener
}
// NewServer constructs an authenticated gRPC server for the supplied listener
// NewServer constructs an authenticated edge server for the supplied listener
// configuration and dependency bundle. Nil dependencies are replaced with safe
// defaults so the gateway can expose the documented transport surface with the
// full auth pipeline wired from built-in fallbacks.
@@ -128,17 +140,17 @@ func NewServer(cfg config.AuthenticatedGRPCConfig, deps ServerDependencies) *Ser
deps.SessionCache,
),
),
logger: deps.Logger.Named("authenticated_grpc"),
logger: deps.Logger.Named("authenticated_edge"),
pushHub: deps.PushHub,
metrics: deps.Telemetry,
}
}
// Run binds the configured listener and serves the authenticated gRPC surface
// until Shutdown closes the server.
// Run binds the configured listener and serves the authenticated edge
// surface until Shutdown closes the server.
func (s *Server) Run(ctx context.Context) error {
if ctx == nil {
return errors.New("run authenticated gRPC server: nil context")
return errors.New("run authenticated edge server: nil context")
}
if err := ctx.Err(); err != nil {
return err
@@ -146,23 +158,30 @@ func (s *Server) Run(ctx context.Context) error {
listener, err := net.Listen("tcp", s.cfg.Addr)
if err != nil {
return fmt.Errorf("run authenticated gRPC server: listen on %q: %w", s.cfg.Addr, err)
return fmt.Errorf("run authenticated edge server: listen on %q: %w", s.cfg.Addr, err)
}
grpcServer := grpc.NewServer(
grpc.ConnectionTimeout(s.cfg.ConnectionTimeout),
grpc.StatsHandler(otelgrpc.NewServerHandler()),
grpc.ChainUnaryInterceptor(observabilityUnaryInterceptor(s.logger, s.metrics)),
grpc.ChainStreamInterceptor(observabilityStreamInterceptor(s.logger, s.metrics)),
mux := http.NewServeMux()
connectHandler := newConnectEdgeAdapter(s.service)
path, handler := gatewayv1connect.NewEdgeGatewayHandler(
connectHandler,
connect.WithInterceptors(observabilityConnectInterceptor(s.logger, s.metrics)),
)
gatewayv1.RegisterEdgeGatewayServer(grpcServer, s.service)
mux.Handle(path, handler)
tracedHandler := otelhttp.NewHandler(mux, "authenticated_edge")
http2Server := &http2.Server{IdleTimeout: s.cfg.ConnectionTimeout}
httpServer := &http.Server{
Handler: h2c.NewHandler(tracedHandler, http2Server),
ReadHeaderTimeout: s.cfg.ConnectionTimeout,
}
s.stateMu.Lock()
s.server = grpcServer
s.server = httpServer
s.listener = listener
s.stateMu.Unlock()
s.logger.Info("authenticated gRPC server started", zap.String("addr", listener.Addr().String()))
s.logger.Info("authenticated edge server started", zap.String("addr", listener.Addr().String()))
defer func() {
s.stateMu.Lock()
@@ -171,24 +190,22 @@ func (s *Server) Run(ctx context.Context) error {
s.stateMu.Unlock()
}()
err = grpcServer.Serve(listener)
err = httpServer.Serve(listener)
switch {
case err == nil:
return nil
case errors.Is(err, grpc.ErrServerStopped):
s.logger.Info("authenticated gRPC server stopped")
case err == nil, errors.Is(err, http.ErrServerClosed):
s.logger.Info("authenticated edge server stopped")
return nil
default:
return fmt.Errorf("run authenticated gRPC server: serve on %q: %w", s.cfg.Addr, err)
return fmt.Errorf("run authenticated edge server: serve on %q: %w", s.cfg.Addr, err)
}
}
// Shutdown gracefully stops the authenticated gRPC server within ctx. When the
// graceful stop exceeds ctx, the server is force-stopped before returning the
// Shutdown gracefully stops the authenticated edge server within ctx. When the
// graceful stop exceeds ctx, the server is force-closed before returning the
// timeout to the caller.
func (s *Server) Shutdown(ctx context.Context) error {
if ctx == nil {
return errors.New("shutdown authenticated gRPC server: nil context")
return errors.New("shutdown authenticated edge server: nil context")
}
s.stateMu.RLock()
@@ -203,20 +220,16 @@ func (s *Server) Shutdown(ctx context.Context) error {
s.pushHub.Shutdown()
}
stopped := make(chan struct{})
go func() {
server.GracefulStop()
close(stopped)
}()
select {
case <-stopped:
err := server.Shutdown(ctx)
if err == nil {
return nil
case <-ctx.Done():
server.Stop()
<-stopped
return fmt.Errorf("shutdown authenticated gRPC server: %w", ctx.Err())
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
_ = server.Close()
return fmt.Errorf("shutdown authenticated edge server: %w", err)
}
return fmt.Errorf("shutdown authenticated edge server: %w", err)
}
func (s *Server) listenAddr() string {