A route may have its own listener: every request there is that route, paths unprefixed

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-25 19:59:21 -07:00
parent 35b07ece81
commit fa1c398fc4
10 changed files with 392 additions and 12 deletions
+69 -8
View File
@@ -11,6 +11,7 @@ import (
"net/http"
"os"
"os/signal"
"sort"
"syscall"
"time"
@@ -94,21 +95,56 @@ func run() error {
p := proxy.New(cfg, table, leases, lim, st, log)
p.SetWaker(waker)
var handler http.Handler = p
// Identity: gate the proxy on the route's peers when a backend is
// configured; off leaves the proxy unwrapped. The same checker gates each
// route's dedicated listener.
logIdentityMode(log, cfg.Identity)
var checker *identity.Checker
if cfg.Identity != "off" {
var checker *identity.Checker
switch cfg.Identity {
case "tailscale":
checker = identity.NewChecker(identity.TailscaleResolver{})
default: // "header"
checker = identity.NewHeaderChecker()
}
}
var handler http.Handler = p
if checker != nil {
handler = identity.Middleware(checker, func(route string) ([]string, bool) {
rt, _, ok := cfg.Route(route)
return rt.Peers, ok
}, p)
}
// Routes with a dedicated listener each serve their own address, with every request there being
// that route and the path unprefixed. In sorted route order, one server each, the proxy's
// ForRoute handler wrapped in RouteMiddleware when identity is on. No admin mux on them.
type routeServer struct {
route string
srv *http.Server
}
var routes []routeServer
listens := make([]string, 0, len(cfg.Routes))
for name := range cfg.Routes {
if cfg.Routes[name].Listen != "" {
listens = append(listens, name)
}
}
sort.Strings(listens)
for _, name := range listens {
rt := cfg.Routes[name]
var h http.Handler = p.ForRoute(name)
if checker != nil {
h = identity.RouteMiddleware(checker, rt.Peers, h)
}
routes = append(routes, routeServer{route: name, srv: &http.Server{
Addr: rt.Listen,
Handler: h,
ReadHeaderTimeout: 10 * time.Second,
}})
}
mux := http.NewServeMux()
mux.Handle("/_crossbar/", admin.Handler(cfg, table, leases, lim, st, hosts))
mux.Handle("/", handler)
@@ -173,22 +209,47 @@ func run() error {
ReadHeaderTimeout: 10 * time.Second,
}
serverErr := make(chan error, 1)
go func() {
log.Info("listening", "addr", srv.Addr)
serverErr <- srv.ListenAndServe()
}()
// Every listener shuts down together on ctx done; the first error other than a clean shutdown
// ends run and shuts the rest down.
servers := make([]*http.Server, 0, 1+len(routes))
servers = append(servers, srv)
for i := range routes {
servers = append(servers, routes[i].srv)
}
serverErr := make(chan error, len(servers))
start := func(s *http.Server, route string) {
go func() {
if route != "" {
log.Info("listening", "addr", s.Addr, "route", route)
} else {
log.Info("listening", "addr", s.Addr)
}
serverErr <- s.ListenAndServe()
}()
}
start(srv, "")
for _, rs := range routes {
start(rs.srv, rs.route)
}
select {
case <-ctx.Done():
log.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
for _, s := range servers {
_ = s.Shutdown(shutdownCtx)
}
return nil
case err := <-serverErr:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for _, s := range servers {
_ = s.Shutdown(shutdownCtx)
}
return err
}
}