Files
kyle fa1c398fc4 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)
2026-09-25 19:59:21 -07:00

87 lines
2.7 KiB
Go

// Package identity gates a route on the set of tailnet peers allowed to use it.
// The middleware sits in front of the proxy: it names the route the same way the
// proxy does and refuses with 403 any caller a route does not allow.
package identity
import (
"net/http"
"strings"
)
const (
// routeHeader is how a caller names the route, the same header the proxy
// reads.
routeHeader = "X-Crossbar-Route"
// peerHeader carries the node name in header mode.
peerHeader = "X-Crossbar-Peer"
// adminPrefix is never gated here; the proxy's own handlers own it.
adminPrefix = "/_crossbar/"
)
// Middleware wraps next with the peer gate. peersFor names the allow list for a
// route and reports whether it knows the route; an unknown route, like a path
// under adminPrefix, passes straight through.
func Middleware(c *Checker, peersFor func(route string) ([]string, bool), next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, adminPrefix) {
next.ServeHTTP(w, r)
return
}
route := r.Header.Get(routeHeader)
if route == "" {
route = firstSegment(r.URL.Path)
}
peers, known := peersFor(route)
if !known {
next.ServeHTTP(w, r)
return
}
ctx := WithHeaderPeer(r.Context(), r.Header.Get(peerHeader))
if err := c.Allow(ctx, peers, r.RemoteAddr); err != nil {
writeForbidden(w)
return
}
next.ServeHTTP(w, r)
})
}
// RouteMiddleware gates every request on a fixed set of peers, whatever path or X-Crossbar-Route
// header it carries: on a route's dedicated listener the route is fixed, so the gate is that route's
// peers for every request. There is no admin-path exemption — there is no admin on a route listener.
// Empty peers lets everyone through, as for Middleware.
func RouteMiddleware(c *Checker, peers []string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := WithHeaderPeer(r.Context(), r.Header.Get(peerHeader))
if err := c.Allow(ctx, peers, r.RemoteAddr); err != nil {
writeForbidden(w)
return
}
next.ServeHTTP(w, r)
})
}
// writeForbidden answers the JSON 403 the tests and callers expect.
func writeForbidden(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(`{"error":"forbidden route"}`))
}
// firstSegment takes the first path segment as the route, "/a/v1/x" -> "a".
func firstSegment(path string) string {
if path == "" || path[0] != '/' {
return ""
}
after := path[1:]
if slash := strings.IndexByte(after, '/'); slash >= 0 {
if after[:slash] == "" {
return ""
}
return after[:slash]
}
if after == "" {
return ""
}
return after
}