mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-20 07:21:27 +02:00
* [agent-network] Shared proto, OpenAPI schema, and generated types * [agent-network] Management: store, manager, synthesizer, policy engine, provider catalog, HTTP/gRPC API Adds the account-scoped agent-network module: provider/policy/budget CRUD and store, the reverse-proxy service synthesizer, policy selection + limit enforcement, the provider catalog (incl. Vertex AI and AWS Bedrock entries), and the management HTTP + proxy gRPC surfaces. * [management] Fix agent-network proxy-peer fan-out on affected-peer recompute The affected-peers resolver loaded only persisted reverse-proxy services, but agent-network services are synthesized on demand and never persisted. As a result the embedded proxy peer was never folded into the affected set when a client's group changed, so the proxy received no network-map update for a newly authorised client and rejected its handshake until a full resync (restart). loadProxyServices now merges the synthesized agent-network services (injected via a registration hook to avoid an import cycle), so proxy peers learn newly authorised clients immediately. * [proxy] Reverse-proxy middleware framework, chain, and request plumbing The per-target middleware chain (slots, dispatcher, mutation gate, metadata merger), body capture, access-log terminal sink, and the proxy wiring that builds + runs chains for synthesized agent-network services. * [proxy] LLM parsers, pricing, and builtin middlewares (OpenAI, Anthropic, Vertex AI, AWS Bedrock) Request/response parsers and SSE/event-stream metering, the embedded pricing table, and the builtin middleware set: request parser, router, policy limit-check/record, cost meter, guardrail, identity inject, response parser. Includes the path-routed providers — Google Vertex AI (keyfile:: service-account OAuth minting) and AWS Bedrock (bearer auth, invoke/converse/streaming, optional /bedrock prefix) — plus the Models allowlist and unmeterable-publisher deny. * [proxy] IPv6 in-place apply and TCP accept-loop hardening on netstack listeners * [agent-network] End-to-end test suite, module docs, and deployment preset * [agent-network] Fix codespell typos and exclude false positives - labelgen word pool: vermillion -> vermilion, racoon -> raccoon. - codespell ignore list: add flate (Go compress/flate package), recordin (a test-local identifier), and unparseable (a valid alternative spelling used consistently across identifiers + a metadata-value constant). * [management] Set LastSeen on injected proxy peer in realstack test (MySQL strict-mode) The injected embedded proxy peer had a PeerStatus with a zero LastSeen, which serializes to '0000-00-00' and is rejected by MySQL in strict mode (SQLite tolerates it). Set LastSeen to a valid time so SaveAccount succeeds on both engines. * [agent-network] Remove e2e shell-script suite from this branch The end-to-end shell scripts under scripts/e2e/ are maintained in a separate testing suite and are not part of this change set. * [agent-network] Polish module docs: remove internal review scaffolding, fix links, verify diagrams Strip PR-review framing, commit references, absolute paths, and stale internal references from the agent-network module docs; fix broken relative links; verify all diagrams against the current architecture. Remove the internal AI-reviewer prompt file. * [management] Refine session expiration handling to support 3-state encoding for SSO deadlines * [agent-network] Relocate agentnetwork package to internals/modules Move management/server/agentnetwork (and its catalog/, labelgen/, types/ subpackages) to management/internals/modules/agentnetwork, alongside the reverse-proxy module, and rewrite all importers. Pure relocation: package names, the synthesizer + affectedpeers registration hook, and store access (shared store.Store) are unchanged, so no import cycle is introduced (affectedpeers still depends only on the agentnetwork/types leaf). * [agent-network] Co-locate HTTP handlers in the module (RegisterEndpoints) Move the agent-network HTTP handlers from server/http/handlers/agentnetwork into the module at internals/modules/agentnetwork/handlers (package handlers) and rename the entrypoint AddEndpoints -> RegisterEndpoints, matching the reverse-proxy module convention. Wiring in http/handler.go updated accordingly.
305 lines
8.0 KiB
Go
305 lines
8.0 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"maps"
|
|
"net/netip"
|
|
"sync"
|
|
|
|
"github.com/netbirdio/netbird/proxy/internal/types"
|
|
)
|
|
|
|
type requestContextKey string
|
|
|
|
const (
|
|
capturedDataKey requestContextKey = "capturedData"
|
|
)
|
|
|
|
// ResponseOrigin indicates where a response was generated.
|
|
type ResponseOrigin int
|
|
|
|
const (
|
|
// OriginBackend means the response came from the backend service.
|
|
OriginBackend ResponseOrigin = iota
|
|
// OriginNoRoute means the proxy had no matching host or path.
|
|
OriginNoRoute
|
|
// OriginProxyError means the proxy failed to reach the backend.
|
|
OriginProxyError
|
|
// OriginAuth means the proxy intercepted the request for authentication.
|
|
OriginAuth
|
|
)
|
|
|
|
func (o ResponseOrigin) String() string {
|
|
switch o {
|
|
case OriginNoRoute:
|
|
return "no_route"
|
|
case OriginProxyError:
|
|
return "proxy_error"
|
|
case OriginAuth:
|
|
return "auth"
|
|
default:
|
|
return "backend"
|
|
}
|
|
}
|
|
|
|
// CapturedData is a mutable struct that allows downstream handlers
|
|
// to pass data back up the middleware chain.
|
|
type CapturedData struct {
|
|
mu sync.RWMutex
|
|
requestID string
|
|
serviceID types.ServiceID
|
|
accountID types.AccountID
|
|
origin ResponseOrigin
|
|
clientIP netip.Addr
|
|
userID string
|
|
userEmail string
|
|
userGroups []string
|
|
// userGroupNames pairs positionally with userGroups; populated from
|
|
// the JWT's group_names claim or from ValidateSession/Tunnel
|
|
// responses. Slice may be shorter than userGroups for tokens minted
|
|
// before names were resolvable.
|
|
userGroupNames []string
|
|
authMethod string
|
|
metadata map[string]string
|
|
agentNetwork bool
|
|
suppressAccessLog bool
|
|
}
|
|
|
|
// NewCapturedData creates a CapturedData with the given request ID.
|
|
func NewCapturedData(requestID string) *CapturedData {
|
|
return &CapturedData{requestID: requestID}
|
|
}
|
|
|
|
// GetRequestID returns the request ID.
|
|
func (c *CapturedData) GetRequestID() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.requestID
|
|
}
|
|
|
|
// SetServiceID sets the service ID.
|
|
func (c *CapturedData) SetServiceID(serviceID types.ServiceID) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.serviceID = serviceID
|
|
}
|
|
|
|
// GetServiceID returns the service ID.
|
|
func (c *CapturedData) GetServiceID() types.ServiceID {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.serviceID
|
|
}
|
|
|
|
// SetAccountID sets the account ID.
|
|
func (c *CapturedData) SetAccountID(accountID types.AccountID) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.accountID = accountID
|
|
}
|
|
|
|
// GetAccountID returns the account ID.
|
|
func (c *CapturedData) GetAccountID() types.AccountID {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.accountID
|
|
}
|
|
|
|
// SetOrigin sets the response origin.
|
|
func (c *CapturedData) SetOrigin(origin ResponseOrigin) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.origin = origin
|
|
}
|
|
|
|
// GetOrigin returns the response origin.
|
|
func (c *CapturedData) GetOrigin() ResponseOrigin {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.origin
|
|
}
|
|
|
|
// SetClientIP sets the resolved client IP.
|
|
func (c *CapturedData) SetClientIP(ip netip.Addr) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.clientIP = ip
|
|
}
|
|
|
|
// GetClientIP returns the resolved client IP.
|
|
func (c *CapturedData) GetClientIP() netip.Addr {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.clientIP
|
|
}
|
|
|
|
// SetUserID sets the authenticated user ID.
|
|
func (c *CapturedData) SetUserID(userID string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.userID = userID
|
|
}
|
|
|
|
// GetUserID returns the authenticated user ID.
|
|
func (c *CapturedData) GetUserID() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.userID
|
|
}
|
|
|
|
// SetUserEmail records the authenticated user's email address. Used by
|
|
// policy-aware middlewares to stamp identity onto upstream requests
|
|
// (e.g. x-litellm-end-user-id) without a management round-trip.
|
|
func (c *CapturedData) SetUserEmail(email string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.userEmail = email
|
|
}
|
|
|
|
// GetUserEmail returns the authenticated user's email address. Returns
|
|
// the empty string when the auth path didn't carry an email (e.g.
|
|
// non-OIDC schemes or legacy JWTs minted before the email claim).
|
|
func (c *CapturedData) GetUserEmail() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.userEmail
|
|
}
|
|
|
|
// SetUserGroups records the authenticated user's group memberships so
|
|
// downstream policy-aware middlewares can authorise the request without
|
|
// an additional management round-trip. The auth middleware populates this
|
|
// from ValidateSessionResponse / ValidateTunnelPeerResponse and from the
|
|
// session JWT's groups claim on cookie-bearing requests.
|
|
func (c *CapturedData) SetUserGroups(groups []string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(groups) == 0 {
|
|
c.userGroups = nil
|
|
return
|
|
}
|
|
c.userGroups = append(c.userGroups[:0], groups...)
|
|
}
|
|
|
|
// SetAgentNetwork records whether the request hit a synthesised
|
|
// agent-network target. The terminal access-log middleware stamps the
|
|
// flag onto the proto so management can distinguish synthetic traffic.
|
|
func (c *CapturedData) SetAgentNetwork(b bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.agentNetwork = b
|
|
}
|
|
|
|
// GetAgentNetwork reports whether the request matched a synthesised
|
|
// agent-network target.
|
|
func (c *CapturedData) GetAgentNetwork() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.agentNetwork
|
|
}
|
|
|
|
// SetSuppressAccessLog records whether the per-request access-log emission
|
|
// must be skipped for this request. Stamped from the matched target's
|
|
// DisableAccessLog flag so the access-log middleware can short-circuit
|
|
// log delivery for opted-out agent-network targets.
|
|
func (c *CapturedData) SetSuppressAccessLog(b bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.suppressAccessLog = b
|
|
}
|
|
|
|
// GetSuppressAccessLog reports whether access-log emission has been
|
|
// suppressed for this request.
|
|
func (c *CapturedData) GetSuppressAccessLog() bool {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.suppressAccessLog
|
|
}
|
|
|
|
// GetUserGroups returns a copy of the authenticated user's group
|
|
// memberships.
|
|
func (c *CapturedData) GetUserGroups() []string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if len(c.userGroups) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, len(c.userGroups))
|
|
copy(out, c.userGroups)
|
|
return out
|
|
}
|
|
|
|
// SetUserGroupNames records the human-readable display names for the
|
|
// user's groups, ordered identically to UserGroups (positional
|
|
// pairing). Stamped onto upstream requests as X-NetBird-Groups so
|
|
// downstream services can read names rather than opaque ids.
|
|
func (c *CapturedData) SetUserGroupNames(names []string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(names) == 0 {
|
|
c.userGroupNames = nil
|
|
return
|
|
}
|
|
c.userGroupNames = append(c.userGroupNames[:0], names...)
|
|
}
|
|
|
|
// GetUserGroupNames returns a copy of the authenticated user's group
|
|
// display names. Position i pairs with UserGroups[i]. May be shorter
|
|
// than UserGroups for tokens minted before names were resolvable; the
|
|
// consumer should fall back to ids for missing positions.
|
|
func (c *CapturedData) GetUserGroupNames() []string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
if len(c.userGroupNames) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]string, len(c.userGroupNames))
|
|
copy(out, c.userGroupNames)
|
|
return out
|
|
}
|
|
|
|
// SetAuthMethod sets the authentication method used.
|
|
func (c *CapturedData) SetAuthMethod(method string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.authMethod = method
|
|
}
|
|
|
|
// GetAuthMethod returns the authentication method used.
|
|
func (c *CapturedData) GetAuthMethod() string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.authMethod
|
|
}
|
|
|
|
// SetMetadata sets a key-value pair in the metadata map.
|
|
func (c *CapturedData) SetMetadata(key, value string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.metadata == nil {
|
|
c.metadata = make(map[string]string)
|
|
}
|
|
c.metadata[key] = value
|
|
}
|
|
|
|
// GetMetadata returns a copy of the metadata map.
|
|
func (c *CapturedData) GetMetadata() map[string]string {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return maps.Clone(c.metadata)
|
|
}
|
|
|
|
// WithCapturedData adds a CapturedData struct to the context.
|
|
func WithCapturedData(ctx context.Context, data *CapturedData) context.Context {
|
|
return context.WithValue(ctx, capturedDataKey, data)
|
|
}
|
|
|
|
// CapturedDataFromContext retrieves the CapturedData from context.
|
|
func CapturedDataFromContext(ctx context.Context) *CapturedData {
|
|
v := ctx.Value(capturedDataKey)
|
|
data, ok := v.(*CapturedData)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return data
|
|
}
|