mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-21 08:09:55 +00:00
Adds a new "private" service mode for the reverse proxy: services reachable exclusively over the embedded WireGuard tunnel, gated by per-peer group membership instead of operator auth schemes. Wire contract - ProxyMapping.private (field 13): the proxy MUST call ValidateTunnelPeer and fail closed; operator schemes are bypassed. - ProxyCapabilities.private (4) + supports_private_service (5): capability gate. Management never streams private mappings to proxies that don't claim the capability; the broadcast path applies the same filter via filterMappingsForProxy. - ValidateTunnelPeer RPC: resolves an inbound tunnel IP to a peer, checks the peer's groups against service.AccessGroups, and mints a session JWT on success. checkPeerGroupAccess fails closed when a private service has empty AccessGroups. - ValidateSession/ValidateTunnelPeer responses now carry peer_group_ids + peer_group_names so the proxy can authorise policy-aware middlewares without an extra management round-trip. - ProxyInboundListener + SendStatusUpdate.inbound_listener: per-account inbound listener state surfaced to dashboards. - PathTargetOptions.direct_upstream (11): bypass the embedded NetBird client and dial the target via the proxy host's network stack for upstreams reachable without WireGuard. Data model - Service.Private (bool) + Service.AccessGroups ([]string, JSON- serialised). Validate() rejects bearer auth on private services. Copy() deep-copies AccessGroups. pgx getServices loads the columns. - DomainConfig.Private threaded into the proxy auth middleware. Request handler routes private services through forwardWithTunnelPeer and returns 403 on validation failure. - Account-level SynthesizePrivateServiceZones (synthetic DNS) and injectPrivateServicePolicies (synthetic ACL) gate on len(svc.AccessGroups) > 0. Proxy - /netbird proxy --private (embedded mode) flag; Config.Private in proxy/lifecycle.go. - Per-account inbound listener (proxy/inbound.go) binding HTTP/HTTPS on the embedded NetBird client's WireGuard tunnel netstack. - proxy/internal/auth/tunnel_cache: ValidateTunnelPeer response cache with single-flight de-duplication and per-account eviction. - Local peerstore short-circuit: when the inbound IP isn't in the account roster, deny fast without an RPC. - proxy/server.go reports SupportsPrivateService=true and redacts the full ProxyMapping JSON from info logs (auth_token + header-auth hashed values now only at debug level). Identity forwarding - ValidateSessionJWT returns user_id, email, method, groups, group_names. sessionkey.Claims carries Email + Groups + GroupNames so the proxy can stamp identity onto upstream requests without an extra management round-trip on every cookie-bearing request. - CapturedData carries userEmail / userGroups / userGroupNames; the proxy stamps X-NetBird-User and X-NetBird-Groups on r.Out from the authenticated identity (strips client-supplied values first to prevent spoofing). - AccessLog.UserGroups: access-log enrichment captures the user's group memberships at write time so the dashboard can render group context without reverse-resolving stale memberships. OpenAPI/dashboard surface - ReverseProxyService gains private + access_groups; ReverseProxyCluster gains private + supports_private. ReverseProxyTarget target_type enum gains "cluster". ServiceTargetOptions gains direct_upstream. ProxyAccessLog gains user_groups.
187 lines
6.9 KiB
Go
187 lines
6.9 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"go.opentelemetry.io/otel/metric"
|
|
|
|
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
|
|
)
|
|
|
|
// store defines the interface for proxy persistence operations
|
|
type store interface {
|
|
SaveProxy(ctx context.Context, p *proxy.Proxy) error
|
|
DisconnectProxy(ctx context.Context, proxyID, sessionID string) error
|
|
UpdateProxyHeartbeat(ctx context.Context, p *proxy.Proxy) error
|
|
GetActiveProxyClusterAddresses(ctx context.Context) ([]string, error)
|
|
GetActiveProxyClusterAddressesForAccount(ctx context.Context, accountID string) ([]string, error)
|
|
GetProxyClusters(ctx context.Context, accountID string) ([]proxy.Cluster, error)
|
|
GetClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
|
GetClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
|
GetClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
|
GetClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool
|
|
CleanupStaleProxies(ctx context.Context, inactivityDuration time.Duration) error
|
|
GetProxyByAccountID(ctx context.Context, accountID string) (*proxy.Proxy, error)
|
|
CountProxiesByAccountID(ctx context.Context, accountID string) (int64, error)
|
|
IsClusterAddressConflicting(ctx context.Context, clusterAddress, accountID string) (bool, error)
|
|
DeleteAccountCluster(ctx context.Context, clusterAddress, accountID string) error
|
|
}
|
|
|
|
// Manager handles all proxy operations
|
|
type Manager struct {
|
|
store store
|
|
metrics *metrics
|
|
}
|
|
|
|
// NewManager creates a new proxy Manager
|
|
func NewManager(store store, meter metric.Meter) (*Manager, error) {
|
|
m, err := newMetrics(meter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Manager{
|
|
store: store,
|
|
metrics: m,
|
|
}, nil
|
|
}
|
|
|
|
// Connect registers a new proxy connection in the database.
|
|
// capabilities may be nil for old proxies that do not report them.
|
|
func (m *Manager) Connect(ctx context.Context, proxyID, sessionID, clusterAddress, ipAddress string, accountID *string, capabilities *proxy.Capabilities) (*proxy.Proxy, error) {
|
|
now := time.Now()
|
|
var caps proxy.Capabilities
|
|
if capabilities != nil {
|
|
caps = *capabilities
|
|
}
|
|
p := &proxy.Proxy{
|
|
ID: proxyID,
|
|
SessionID: sessionID,
|
|
ClusterAddress: clusterAddress,
|
|
IPAddress: ipAddress,
|
|
AccountID: accountID,
|
|
LastSeen: now,
|
|
ConnectedAt: &now,
|
|
Status: proxy.StatusConnected,
|
|
Capabilities: caps,
|
|
}
|
|
|
|
if err := m.store.SaveProxy(ctx, p); err != nil {
|
|
log.WithContext(ctx).Errorf("failed to register proxy %s: %v", proxyID, err)
|
|
return nil, err
|
|
}
|
|
|
|
log.WithContext(ctx).WithFields(log.Fields{
|
|
"proxyID": proxyID,
|
|
"sessionID": sessionID,
|
|
"clusterAddress": clusterAddress,
|
|
"ipAddress": ipAddress,
|
|
}).Info("proxy connected")
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// Disconnect marks a proxy as disconnected in the database.
|
|
func (m *Manager) Disconnect(ctx context.Context, proxyID, sessionID string) error {
|
|
if err := m.store.DisconnectProxy(ctx, proxyID, sessionID); err != nil {
|
|
log.WithContext(ctx).Errorf("failed to disconnect proxy %s session %s: %v", proxyID, sessionID, err)
|
|
return err
|
|
}
|
|
|
|
log.WithContext(ctx).WithFields(log.Fields{
|
|
"proxyID": proxyID,
|
|
"sessionID": sessionID,
|
|
}).Info("proxy disconnected")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Heartbeat updates the proxy's last seen timestamp.
|
|
func (m *Manager) Heartbeat(ctx context.Context, p *proxy.Proxy) error {
|
|
if err := m.store.UpdateProxyHeartbeat(ctx, p); err != nil {
|
|
log.WithContext(ctx).Debugf("failed to update proxy %s heartbeat: %v", p.ID, err)
|
|
return err
|
|
}
|
|
|
|
log.WithContext(ctx).Tracef("updated heartbeat for proxy %s session %s", p.ID, p.SessionID)
|
|
m.metrics.IncrementProxyHeartbeatCount()
|
|
return nil
|
|
}
|
|
|
|
// GetActiveClusterAddresses returns all unique cluster addresses for active proxies
|
|
func (m *Manager) GetActiveClusterAddresses(ctx context.Context) ([]string, error) {
|
|
addresses, err := m.store.GetActiveProxyClusterAddresses(ctx)
|
|
if err != nil {
|
|
log.WithContext(ctx).Errorf("failed to get active proxy cluster addresses: %v", err)
|
|
return nil, err
|
|
}
|
|
return addresses, nil
|
|
}
|
|
|
|
// ClusterSupportsCustomPorts returns whether any active proxy in the cluster
|
|
// supports custom ports. Returns nil when no proxy has reported capabilities.
|
|
func (m Manager) ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool {
|
|
return m.store.GetClusterSupportsCustomPorts(ctx, clusterAddr)
|
|
}
|
|
|
|
// ClusterRequireSubdomain returns whether any active proxy in the cluster
|
|
// requires a subdomain. Returns nil when no proxy has reported capabilities.
|
|
func (m Manager) ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool {
|
|
return m.store.GetClusterRequireSubdomain(ctx, clusterAddr)
|
|
}
|
|
|
|
// ClusterSupportsCrowdSec returns whether all active proxies in the cluster
|
|
// have CrowdSec configured (unanimous). Returns nil when no proxy has reported capabilities.
|
|
func (m Manager) ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool {
|
|
return m.store.GetClusterSupportsCrowdSec(ctx, clusterAddr)
|
|
}
|
|
|
|
// ClusterSupportsPrivate reports whether any active proxy claims the private capability (nil = unreported).
|
|
func (m Manager) ClusterSupportsPrivate(ctx context.Context, clusterAddr string) *bool {
|
|
return m.store.GetClusterSupportsPrivate(ctx, clusterAddr)
|
|
}
|
|
|
|
// CleanupStale removes proxies that haven't sent heartbeat in the specified duration
|
|
func (m *Manager) CleanupStale(ctx context.Context, inactivityDuration time.Duration) error {
|
|
if err := m.store.CleanupStaleProxies(ctx, inactivityDuration); err != nil {
|
|
log.WithContext(ctx).Errorf("failed to cleanup stale proxies: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) GetActiveClusterAddressesForAccount(ctx context.Context, accountID string) ([]string, error) {
|
|
addresses, err := m.store.GetActiveProxyClusterAddressesForAccount(ctx, accountID)
|
|
if err != nil {
|
|
log.WithContext(ctx).Errorf("failed to get active proxy cluster addresses for account %s: %v", accountID, err)
|
|
return nil, err
|
|
}
|
|
return addresses, nil
|
|
}
|
|
|
|
func (m *Manager) GetAccountProxy(ctx context.Context, accountID string) (*proxy.Proxy, error) {
|
|
return m.store.GetProxyByAccountID(ctx, accountID)
|
|
}
|
|
|
|
func (m *Manager) CountAccountProxies(ctx context.Context, accountID string) (int64, error) {
|
|
return m.store.CountProxiesByAccountID(ctx, accountID)
|
|
}
|
|
|
|
func (m *Manager) IsClusterAddressAvailable(ctx context.Context, clusterAddress, accountID string) (bool, error) {
|
|
conflicting, err := m.store.IsClusterAddressConflicting(ctx, clusterAddress, accountID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return !conflicting, nil
|
|
}
|
|
|
|
func (m *Manager) DeleteAccountCluster(ctx context.Context, clusterAddress, accountID string) error {
|
|
if err := m.store.DeleteAccountCluster(ctx, clusterAddress, accountID); err != nil {
|
|
log.WithContext(ctx).Errorf("failed to delete cluster %s for account %s: %v", clusterAddress, accountID, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|