feat: tunnel JWT auth

Tunnel method now requires Bearer JWT (HMAC-HS256) in gRPC metadata.
   - internal/auth: pure-stdlib verifier + issuer + revoked-jti list
   - internal/tunnel: per-target rate limiter + egress allowlist with
     wildcard support + crypto/rand frame padding to {1KB, 4KB, 16KB}
   - cmd/server reads /etc/stats-gateway/{jwt.key,tunnel.yaml,revoked.txt}
     via --config-dir flag
   - cmd/issue-token Go binary wraps auth.Issue
   - configs/tunnel.yaml example with projectshitpost wildcard


Tests: 13 PASS across 5 packages.
This commit is contained in:
2026-05-05 21:28:37 +05:00
parent 0e3a3b4b0e
commit 731b60863a
12 changed files with 571 additions and 11 deletions
+32 -5
View File
@@ -2,23 +2,30 @@ package service
import (
"context"
"strings"
"time"
statsv1 "github.com/bergabruh/stats-gateway/gen/stats/v1"
"github.com/bergabruh/stats-gateway/internal/auth"
"github.com/bergabruh/stats-gateway/internal/storage"
"github.com/bergabruh/stats-gateway/internal/tunnel"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// Service implements statsv1.SpeedStatusServer with public RecentResults
// and Aggregate methods. Tunnel returns Unimplemented until Task 11 wires
// auth + handler.
// Service implements statsv1.SpeedStatusServer.
type Service struct {
statsv1.UnimplementedSpeedStatusServer
store *storage.Store
store *storage.Store
verifier *auth.Verifier
tunnel *tunnel.Handler
}
func New(st *storage.Store) *Service { return &Service{store: st} }
// New creates a Service with the given storage, JWT verifier, and tunnel handler.
func New(st *storage.Store, v *auth.Verifier, t *tunnel.Handler) *Service {
return &Service{store: st, verifier: v, tunnel: t}
}
func (s *Service) RecentResults(f *statsv1.Filter, stream statsv1.SpeedStatus_RecentResultsServer) error {
limit := int(f.Limit)
@@ -87,3 +94,23 @@ func (s *Service) Aggregate(ctx context.Context, r *statsv1.Range) (*statsv1.His
}
return h, nil
}
// Tunnel authenticates the caller via Bearer JWT then delegates to the tunnel handler.
func (s *Service) Tunnel(stream statsv1.SpeedStatus_TunnelServer) error {
md, ok := metadata.FromIncomingContext(stream.Context())
if !ok {
return status.Error(codes.Unauthenticated, "no metadata")
}
authH := md.Get("authorization")
if len(authH) == 0 {
return status.Error(codes.Unauthenticated, "no auth")
}
tok := strings.TrimPrefix(authH[0], "Bearer ")
if tok == authH[0] {
return status.Error(codes.Unauthenticated, "expected Bearer")
}
if _, err := s.verifier.Verify(tok); err != nil {
return status.Errorf(codes.Unauthenticated, "auth: %v", err)
}
return s.tunnel.Serve(stream)
}