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
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"bytes"
"flag"
"fmt"
"log"
"os"
"time"
"github.com/bergabruh/stats-gateway/internal/auth"
)
func main() {
keyPath := flag.String("key", "/etc/stats-gateway/jwt.key", "path to HMAC secret key file")
subject := flag.String("sub", "", "client-id (required)")
days := flag.Int("days", 90, "token validity in days")
flag.Parse()
if *subject == "" {
log.Fatal("--sub required")
}
secret, err := os.ReadFile(*keyPath)
if err != nil {
log.Fatalf("read key: %v", err)
}
tok, err := auth.Issue(bytes.TrimSpace(secret), *subject, "tunnel", time.Duration(*days)*24*time.Hour)
if err != nil {
log.Fatal(err)
}
fmt.Println(tok)
}
+35 -2
View File
@@ -1,20 +1,27 @@
package main
import (
"bytes"
"flag"
"log"
"net"
"os"
"path/filepath"
"google.golang.org/grpc"
"gopkg.in/yaml.v3"
statsv1 "github.com/bergabruh/stats-gateway/gen/stats/v1"
"github.com/bergabruh/stats-gateway/internal/auth"
"github.com/bergabruh/stats-gateway/internal/service"
"github.com/bergabruh/stats-gateway/internal/storage"
"github.com/bergabruh/stats-gateway/internal/tunnel"
)
func main() {
addr := flag.String("addr", "127.0.0.1:50051", "listen address")
dbPath := flag.String("db", "/var/lib/stats-gateway/stats.db", "sqlite path")
cfgDir := flag.String("config-dir", "/etc/stats-gateway", "config dir (jwt.key, tunnel.yaml, revoked.txt)")
flag.Parse()
st, err := storage.Open(*dbPath)
@@ -23,15 +30,41 @@ func main() {
}
defer st.Close()
secret, err := os.ReadFile(filepath.Join(*cfgDir, "jwt.key"))
if err != nil {
log.Fatalf("read jwt.key: %v", err)
}
secret = bytes.TrimSpace(secret)
cfgBytes, err := os.ReadFile(filepath.Join(*cfgDir, "tunnel.yaml"))
if err != nil {
log.Fatalf("read tunnel.yaml: %v", err)
}
var tunnelCfg struct {
Egress tunnel.EgressConfig `yaml:"egress"`
}
if err := yaml.Unmarshal(cfgBytes, &tunnelCfg); err != nil {
log.Fatalf("parse tunnel.yaml: %v", err)
}
revoked, err := auth.LoadRevokedFile(filepath.Join(*cfgDir, "revoked.txt"))
if err != nil {
log.Fatalf("load revoked: %v", err)
}
verifier := auth.NewVerifier(secret, revoked)
handler := tunnel.NewHandler(tunnelCfg.Egress)
lis, err := net.Listen("tcp", *addr)
if err != nil {
log.Fatalf("listen: %v", err)
}
srv := grpc.NewServer()
statsv1.RegisterSpeedStatusServer(srv, service.New(st))
statsv1.RegisterSpeedStatusServer(srv, service.New(st, verifier, handler))
log.Printf("stats-gateway listening on %s", *addr)
log.Printf("stats-gateway listening on %s (egress=%d targets, rate=%.0fmbps)",
*addr, len(tunnelCfg.Egress.Allowed), tunnelCfg.Egress.RateMbps)
if err := srv.Serve(lis); err != nil {
log.Fatalf("serve: %v", err)
}