Merge prototype/reverse-proxy with proxy clustering support

Combines token-based authentication from upstream with proxy clustering:
- Session keys for JWT signing (SessionPrivateKey/SessionPublicKey)
- One-time token store for proxy authentication
- Cluster-targeted updates via SendReverseProxyUpdateToCluster
- ProxyCluster field derived from domain
- OIDC validation config support

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
mlsmaycon
2026-02-05 14:08:07 +01:00
73 changed files with 5239 additions and 1253 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/manager"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
idpmanager "github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/management-integrations/integrations"
@@ -43,6 +44,7 @@ import (
"github.com/netbirdio/netbird/management/server/http/handlers/networks"
"github.com/netbirdio/netbird/management/server/http/handlers/peers"
"github.com/netbirdio/netbird/management/server/http/handlers/policies"
"github.com/netbirdio/netbird/management/server/http/handlers/proxy"
"github.com/netbirdio/netbird/management/server/http/handlers/routes"
"github.com/netbirdio/netbird/management/server/http/handlers/setup_keys"
"github.com/netbirdio/netbird/management/server/http/handlers/users"
@@ -64,7 +66,7 @@ const (
)
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, reverseProxyManager reverseproxy.Manager, reverseProxyDomainManager *domain.Manager, reverseProxyAccessLogsManager accesslogs.Manager) (http.Handler, error) {
func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, reverseProxyManager reverseproxy.Manager, reverseProxyDomainManager *domain.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer) (http.Handler, error) {
// Register bypass paths for unauthenticated endpoints
if err := bypass.AddBypassPath("/api/instance"); err != nil {
@@ -80,6 +82,10 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
if err := bypass.AddBypassPath("/api/users/invites/nbi_*/accept"); err != nil {
return nil, fmt.Errorf("failed to add bypass path: %w", err)
}
// OAuth callback for proxy authentication
if err := bypass.AddBypassPath("/api/oauth/callback"); err != nil {
return nil, fmt.Errorf("failed to add bypass path: %w", err)
}
var rateLimitingConfig *middleware.RateLimiterConfig
if os.Getenv(rateLimitingEnabledKey) == "true" {
@@ -164,6 +170,12 @@ func NewAPIHandler(ctx context.Context, accountManager account.Manager, networks
reverseproxymanager.RegisterEndpoints(reverseProxyManager, *reverseProxyDomainManager, reverseProxyAccessLogsManager, router)
}
// Register OAuth callback handler for proxy authentication
if proxyGRPCServer != nil {
oauthHandler := proxy.NewAuthCallbackHandler(proxyGRPCServer)
oauthHandler.RegisterEndpoints(router)
}
// Mount embedded IdP handler at /oauth2 path if configured
if embeddedIdpEnabled {
rootRouter.PathPrefix("/oauth2").Handler(corsMiddleware.Handler(embeddedIdP.Handler()))

View File

@@ -395,7 +395,7 @@ func (h *Handler) GetAccessiblePeers(w http.ResponseWriter, r *http.Request) {
dnsDomain := h.networkMapController.GetDNSDomain(account.Settings)
netMap := account.GetPeerNetworkMap(r.Context(), peerID, dns.CustomZone{}, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers())
netMap := account.GetPeerNetworkMap(r.Context(), peerID, dns.CustomZone{}, nil, validPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil, account.GetActiveGroupUsers(), account.GetExposedServicesMap(), account.GetProxyPeers())
util.WriteJSONObject(r.Context(), w, toAccessiblePeers(netMap, dnsDomain))
}

View File

@@ -0,0 +1,143 @@
package proxy
import (
"context"
"net/http"
"net/url"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/proxy/auth"
)
type AuthCallbackHandler struct {
proxyService *nbgrpc.ProxyServiceServer
}
func NewAuthCallbackHandler(proxyService *nbgrpc.ProxyServiceServer) *AuthCallbackHandler {
return &AuthCallbackHandler{
proxyService: proxyService,
}
}
func (h *AuthCallbackHandler) RegisterEndpoints(router *mux.Router) {
router.HandleFunc("/oauth/callback", h.handleCallback).Methods(http.MethodGet)
}
func (h *AuthCallbackHandler) handleCallback(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
codeVerifier, originalURL, err := h.proxyService.ValidateState(state)
if err != nil {
log.WithError(err).Error("OAuth callback state validation failed")
http.Error(w, "Invalid state parameter", http.StatusBadRequest)
return
}
redirectURL, err := url.Parse(originalURL)
if err != nil {
log.WithError(err).Error("Failed to parse redirect URL")
http.Error(w, "Invalid redirect URL", http.StatusBadRequest)
return
}
// Get OIDC configuration
oidcConfig := h.proxyService.GetOIDCConfig()
// Create OIDC provider to discover endpoints
provider, err := oidc.NewProvider(r.Context(), oidcConfig.Issuer)
if err != nil {
log.WithError(err).Error("Failed to create OIDC provider")
http.Error(w, "Failed to create OIDC provider", http.StatusInternalServerError)
return
}
token, err := (&oauth2.Config{
ClientID: oidcConfig.ClientID,
Endpoint: provider.Endpoint(),
RedirectURL: oidcConfig.CallbackURL,
}).Exchange(r.Context(), r.URL.Query().Get("code"), oauth2.VerifierOption(codeVerifier))
if err != nil {
log.WithError(err).Error("Failed to exchange code for token")
http.Error(w, "Failed to exchange code for token", http.StatusInternalServerError)
return
}
// Extract user ID from the OIDC token
userID := extractUserIDFromToken(r.Context(), provider, oidcConfig, token)
if userID == "" {
log.Error("Failed to extract user ID from OIDC token")
http.Error(w, "Failed to validate token", http.StatusUnauthorized)
return
}
// Generate session JWT instead of passing OIDC access_token
sessionToken, err := h.proxyService.GenerateSessionToken(r.Context(), redirectURL.Hostname(), userID, auth.MethodOIDC)
if err != nil {
log.WithError(err).Error("Failed to create session token")
http.Error(w, "Failed to create session", http.StatusInternalServerError)
return
}
// Redirect must be HTTPS, regardless of what was originally intended (which should always be HTTPS but better to double-check here).
redirectURL.Scheme = "https"
// Pass the session token in the URL query parameter. The proxy middleware will
// extract it, validate it, set its own cookie, and redirect to remove the token from the URL.
// We cannot set the cookie here because cookies are domain-scoped (RFC 6265) and the
// management server cannot set cookies for the proxy's domain.
query := redirectURL.Query()
query.Set("session_token", sessionToken)
redirectURL.RawQuery = query.Encode()
log.WithField("redirect", redirectURL.Host).Debug("OAuth callback: redirecting user with session token")
http.Redirect(w, r, redirectURL.String(), http.StatusFound)
}
// extractUserIDFromToken extracts the user ID from an OIDC token.
func extractUserIDFromToken(ctx context.Context, provider *oidc.Provider, config nbgrpc.ProxyOIDCConfig, token *oauth2.Token) string {
// Try to get ID token from the oauth2 token extras
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
log.Warn("No id_token in OIDC response")
return ""
}
verifier := provider.Verifier(&oidc.Config{
ClientID: config.ClientID,
})
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
log.WithError(err).Warn("Failed to verify ID token")
return ""
}
// Extract claims
var claims struct {
Subject string `json:"sub"`
Email string `json:"email"`
UserID string `json:"user_id"`
}
if err := idToken.Claims(&claims); err != nil {
log.WithError(err).Warn("Failed to extract claims from ID token")
return ""
}
// Prefer subject, fall back to user_id or email
if claims.Subject != "" {
return claims.Subject
}
if claims.UserID != "" {
return claims.UserID
}
if claims.Email != "" {
return claims.Email
}
return ""
}

View File

@@ -102,7 +102,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, nil, nil, nil)
apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManagerMock, resourcesManagerMock, routersManagerMock, groupsManagerMock, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, nil, nil, nil, nil)
if err != nil {
t.Fatalf("Failed to create API handler: %v", err)
}

View File

@@ -18,7 +18,6 @@ import (
const (
staticClientDashboard = "netbird-dashboard"
staticClientCLI = "netbird-cli"
staticClientProxy = "netbird-proxy"
defaultCLIRedirectURL1 = "http://localhost:53000/"
defaultCLIRedirectURL2 = "http://localhost:54000/"
defaultScopes = "openid profile email groups"
@@ -38,10 +37,8 @@ type EmbeddedIdPConfig struct {
Storage EmbeddedStorageConfig
// DashboardRedirectURIs are the OAuth2 redirect URIs for the dashboard client
DashboardRedirectURIs []string
// CLIRedirectURIs are the OAuth2 redirect URIs for the CLI client
// DashboardRedirectURIs are the OAuth2 redirect URIs for the dashboard client
CLIRedirectURIs []string
// ProxyRedirectURIs are the OAuth2 redirect URIs for the Proxy client
ProxyRedirectURIs []string
// Owner is the initial owner/admin user (optional, can be nil)
Owner *OwnerConfig
// SignKeyRefreshEnabled enables automatic key rotation for signing keys
@@ -89,6 +86,11 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
cliRedirectURIs = append(cliRedirectURIs, "/device/callback")
cliRedirectURIs = append(cliRedirectURIs, c.Issuer+"/device/callback")
// Build dashboard redirect URIs including the OAuth callback for proxy authentication
dashboardRedirectURIs := c.DashboardRedirectURIs
baseURL := strings.TrimSuffix(c.Issuer, "/oauth2")
dashboardRedirectURIs = append(dashboardRedirectURIs, baseURL+"/api/oauth/callback")
cfg := &dex.YAMLConfig{
Issuer: c.Issuer,
Storage: dex.Storage{
@@ -114,7 +116,7 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
ID: staticClientDashboard,
Name: "NetBird Dashboard",
Public: true,
RedirectURIs: c.DashboardRedirectURIs,
RedirectURIs: dashboardRedirectURIs,
},
{
ID: staticClientCLI,
@@ -122,12 +124,6 @@ func (c *EmbeddedIdPConfig) ToYAMLConfig() (*dex.YAMLConfig, error) {
Public: true,
RedirectURIs: cliRedirectURIs,
},
{
ID: staticClientProxy,
Name: "NetBird Proxy",
Public: true,
RedirectURIs: c.ProxyRedirectURIs,
},
},
}
@@ -555,7 +551,7 @@ func (m *EmbeddedIdPManager) GetLocalKeysLocation() string {
// GetClientIDs returns the OAuth2 client IDs configured for this provider.
func (m *EmbeddedIdPManager) GetClientIDs() []string {
return []string{staticClientDashboard, staticClientCLI, staticClientProxy}
return []string{staticClientDashboard, staticClientCLI}
}
// GetUserIDClaim returns the JWT claim name used for user identification.

View File

@@ -545,7 +545,7 @@ func (am *DefaultAccountManager) GetPeerNetwork(ctx context.Context, peerID stri
// Each new Peer will be assigned a new next net.IP from the Account.Network and Account.Network.LastIP will be updated (IP's are not reused).
// The peer property is just a placeholder for the Peer properties to pass further
func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKey, userID string, peer *nbpeer.Peer, temporary bool) (*nbpeer.Peer, *types.NetworkMap, []*posture.Checks, error) {
if setupKey == "" && userID == "" {
if setupKey == "" && userID == "" && !peer.ProxyEmbedded {
// no auth method provided => reject access
return nil, nil, nil, status.Errorf(status.Unauthenticated, "no peer auth method provided, please use a setup key or interactive SSO login")
}
@@ -554,6 +554,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
hashedKey := sha256.Sum256([]byte(upperKey))
encodedHashedKey := b64.StdEncoding.EncodeToString(hashedKey[:])
addedByUser := len(userID) > 0
addedBySetupKey := len(setupKey) > 0
// This is a handling for the case when the same machine (with the same WireGuard pub key) tries to register twice.
// Such case is possible when AddPeer function takes long time to finish after AcquireWriteLockByUID (e.g., database is slow)
@@ -576,7 +577,8 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
var ephemeral bool
var groupsToAdd []string
var allowExtraDNSLabels bool
if addedByUser {
switch {
case addedByUser:
user, err := am.Store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
if err != nil {
return nil, nil, nil, status.Errorf(status.NotFound, "failed adding new peer: user not found")
@@ -599,7 +601,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
}
opEvent.InitiatorID = userID
opEvent.Activity = activity.PeerAddedByUser
} else {
case addedBySetupKey:
// Validate the setup key
sk, err := am.Store.GetSetupKeyBySecret(ctx, store.LockingStrengthNone, encodedHashedKey)
if err != nil {
@@ -622,6 +624,12 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
if !sk.AllowExtraDNSLabels && len(peer.ExtraDNSLabels) > 0 {
return nil, nil, nil, status.Errorf(status.PreconditionFailed, "couldn't add peer: setup key doesn't allow extra DNS labels")
}
default:
if peer.ProxyEmbedded {
log.WithContext(ctx).Debugf("adding peer for proxy embedded, accountID: %s", accountID)
} else {
log.WithContext(ctx).Warnf("adding peer without setup key or userID, accountID: %s", accountID)
}
}
opEvent.AccountID = accountID
@@ -657,6 +665,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
CreatedAt: registrationTime,
LoginExpirationEnabled: addedByUser && !temporary,
Ephemeral: ephemeral,
ProxyEmbedded: peer.ProxyEmbedded,
Location: peer.Location,
InactivityExpirationEnabled: addedByUser && !temporary,
ExtraDNSLabels: peer.ExtraDNSLabels,
@@ -728,12 +737,13 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe
return fmt.Errorf("failed adding peer to All group: %w", err)
}
if addedByUser {
switch {
case addedByUser:
err := transaction.SaveUserLastLogin(ctx, accountID, userID, newPeer.GetLastLogin())
if err != nil {
log.WithContext(ctx).Debugf("failed to update user last login: %v", err)
}
} else {
case addedBySetupKey:
sk, err := transaction.GetSetupKeyBySecret(ctx, store.LockingStrengthUpdate, encodedHashedKey)
if err != nil {
return fmt.Errorf("failed to get setup key: %w", err)

View File

@@ -48,6 +48,8 @@ type Peer struct {
CreatedAt time.Time
// Indicate ephemeral peer attribute
Ephemeral bool `gorm:"index"`
// ProxyEmbedded indicates whether the peer is embedded in a reverse proxy
ProxyEmbedded bool `gorm:"index"`
// Geo location based on connection IP
Location Location `gorm:"embedded;embeddedPrefix:location_"`
@@ -224,6 +226,7 @@ func (p *Peer) Copy() *Peer {
LastLogin: p.LastLogin,
CreatedAt: p.CreatedAt,
Ephemeral: p.Ephemeral,
ProxyEmbedded: p.ProxyEmbedded,
Location: p.Location,
InactivityExpirationEnabled: p.InactivityExpirationEnabled,
ExtraDNSLabels: slices.Clone(p.ExtraDNSLabels),

View File

@@ -1099,6 +1099,7 @@ func (s *SqlStore) getAccountGorm(ctx context.Context, accountID string) (*types
Preload("NetworkRouters").
Preload("NetworkResources").
Preload("Onboarding").
Preload("ReverseProxies").
Take(&account, idQueryCondition, accountID)
if result.Error != nil {
log.WithContext(ctx).Errorf("error when getting account %s from the store: %s", accountID, result.Error)
@@ -1276,6 +1277,17 @@ func (s *SqlStore) getAccountPgx(ctx context.Context, accountID string) (*types.
account.PostureChecks = checks
}()
wg.Add(1)
go func() {
defer wg.Done()
proxies, err := s.getProxies(ctx, accountID)
if err != nil {
errChan <- err
return
}
account.ReverseProxies = proxies
}()
wg.Add(1)
go func() {
defer wg.Done()
@@ -1677,7 +1689,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
meta_kernel_version, meta_network_addresses, meta_system_serial_number, meta_system_product_name, meta_system_manufacturer,
meta_environment, meta_flags, meta_files, peer_status_last_seen, peer_status_connected, peer_status_login_expired,
peer_status_requires_approval, location_connection_ip, location_country_code, location_city_name,
location_geo_name_id FROM peers WHERE account_id = $1`
location_geo_name_id, proxy_embedded FROM peers WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
if err != nil {
return nil, err
@@ -1690,7 +1702,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
lastLogin, createdAt sql.NullTime
sshEnabled, loginExpirationEnabled, inactivityExpirationEnabled, ephemeral, allowExtraDNSLabels sql.NullBool
peerStatusLastSeen sql.NullTime
peerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval sql.NullBool
peerStatusConnected, peerStatusLoginExpired, peerStatusRequiresApproval, proxyEmbedded sql.NullBool
ip, extraDNS, netAddr, env, flags, files, connIP []byte
metaHostname, metaGoOS, metaKernel, metaCore, metaPlatform sql.NullString
metaOS, metaOSVersion, metaWtVersion, metaUIVersion, metaKernelVersion sql.NullString
@@ -1705,7 +1717,7 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
&metaOS, &metaOSVersion, &metaWtVersion, &metaUIVersion, &metaKernelVersion, &netAddr,
&metaSystemSerialNumber, &metaSystemProductName, &metaSystemManufacturer, &env, &flags, &files,
&peerStatusLastSeen, &peerStatusConnected, &peerStatusLoginExpired, &peerStatusRequiresApproval, &connIP,
&locationCountryCode, &locationCityName, &locationGeoNameID)
&locationCountryCode, &locationCityName, &locationGeoNameID, &proxyEmbedded)
if err == nil {
if lastLogin.Valid {
@@ -1789,6 +1801,9 @@ func (s *SqlStore) getPeers(ctx context.Context, accountID string) ([]nbpeer.Pee
if locationGeoNameID.Valid {
p.Location.GeoNameID = uint(locationGeoNameID.Int64)
}
if proxyEmbedded.Valid {
p.ProxyEmbedded = proxyEmbedded.Bool
}
if ip != nil {
_ = json.Unmarshal(ip, &p.IP)
}
@@ -2044,6 +2059,65 @@ func (s *SqlStore) getPostureChecks(ctx context.Context, accountID string) ([]*p
return checks, nil
}
func (s *SqlStore) getProxies(ctx context.Context, accountID string) ([]*reverseproxy.ReverseProxy, error) {
const query = `SELECT id, account_id, name, domain, targets, enabled, auth,
meta_created_at, meta_certificate_issued_at, meta_status
FROM reverse_proxies WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
if err != nil {
return nil, err
}
proxies, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (*reverseproxy.ReverseProxy, error) {
var p reverseproxy.ReverseProxy
var auth []byte
var targets []byte
var createdAt, certIssuedAt sql.NullTime
var status sql.NullString
err := row.Scan(
&p.ID,
&p.AccountID,
&p.Name,
&p.Domain,
&targets,
&p.Enabled,
&auth,
&createdAt,
&certIssuedAt,
&status,
)
if err != nil {
return nil, err
}
// Unmarshal JSON fields
if auth != nil {
if err := json.Unmarshal(auth, &p.Auth); err != nil {
return nil, err
}
}
if targets != nil {
if err := json.Unmarshal(targets, &p.Targets); err != nil {
return nil, err
}
}
p.Meta = reverseproxy.ReverseProxyMeta{}
if createdAt.Valid {
p.Meta.CreatedAt = createdAt.Time
}
if certIssuedAt.Valid {
p.Meta.CertificateIssuedAt = certIssuedAt.Time
}
if status.Valid {
p.Meta.Status = status.String
}
return &p, nil
})
if err != nil {
return nil, err
}
return proxies, nil
}
func (s *SqlStore) getNetworks(ctx context.Context, accountID string) ([]*networkTypes.Network, error) {
const query = `SELECT id, account_id, name, description FROM networks WHERE account_id = $1`
rows, err := s.pool.Query(ctx, query, accountID)
@@ -4609,7 +4683,11 @@ func (s *SqlStore) GetPeerIDByKey(ctx context.Context, lockStrength LockingStren
}
func (s *SqlStore) CreateReverseProxy(ctx context.Context, proxy *reverseproxy.ReverseProxy) error {
result := s.db.Create(proxy)
proxyCopy := proxy.Copy()
if err := proxyCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {
return fmt.Errorf("encrypt reverse proxy data: %w", err)
}
result := s.db.Create(proxyCopy)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to create reverse proxy to store: %v", result.Error)
return status.Errorf(status.Internal, "failed to create reverse proxy to store")
@@ -4619,7 +4697,11 @@ func (s *SqlStore) CreateReverseProxy(ctx context.Context, proxy *reverseproxy.R
}
func (s *SqlStore) UpdateReverseProxy(ctx context.Context, proxy *reverseproxy.ReverseProxy) error {
result := s.db.Select("*").Save(proxy)
proxyCopy := proxy.Copy()
if err := proxyCopy.EncryptSensitiveData(s.fieldEncrypt); err != nil {
return fmt.Errorf("encrypt reverse proxy data: %w", err)
}
result := s.db.Select("*").Save(proxyCopy)
if result.Error != nil {
log.WithContext(ctx).Errorf("failed to update reverse proxy to store: %v", result.Error)
return status.Errorf(status.Internal, "failed to update reverse proxy to store")
@@ -4659,6 +4741,10 @@ func (s *SqlStore) GetReverseProxyByID(ctx context.Context, lockStrength Locking
return nil, status.Errorf(status.Internal, "failed to get reverse proxy from store")
}
if err := proxy.DecryptSensitiveData(s.fieldEncrypt); err != nil {
return nil, fmt.Errorf("decrypt reverse proxy data: %w", err)
}
return proxy, nil
}
@@ -4674,6 +4760,10 @@ func (s *SqlStore) GetReverseProxyByDomain(ctx context.Context, accountID, domai
return nil, status.Errorf(status.Internal, "failed to get reverse proxy by domain from store")
}
if err := proxy.DecryptSensitiveData(s.fieldEncrypt); err != nil {
return nil, fmt.Errorf("decrypt reverse proxy data: %w", err)
}
return proxy, nil
}
@@ -4690,6 +4780,12 @@ func (s *SqlStore) GetReverseProxies(ctx context.Context, lockStrength LockingSt
return nil, status.Errorf(status.Internal, "failed to get reverse proxy from store")
}
for _, proxy := range proxyList {
if err := proxy.DecryptSensitiveData(s.fieldEncrypt); err != nil {
return nil, fmt.Errorf("decrypt reverse proxy data: %w", err)
}
}
return proxyList, nil
}
@@ -4706,6 +4802,12 @@ func (s *SqlStore) GetAccountReverseProxies(ctx context.Context, lockStrength Lo
return nil, status.Errorf(status.Internal, "failed to get reverse proxy from store")
}
for _, proxy := range proxyList {
if err := proxy.DecryptSensitiveData(s.fieldEncrypt); err != nil {
return nil, fmt.Errorf("decrypt reverse proxy data: %w", err)
}
}
return proxyList, nil
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/netbirdio/netbird/client/ssh/auth"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy"
"github.com/netbirdio/netbird/management/internals/modules/zones"
"github.com/netbirdio/netbird/management/internals/modules/zones/records"
resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types"
@@ -99,6 +100,7 @@ type Account struct {
NameServerGroupsG []nbdns.NameServerGroup `json:"-" gorm:"foreignKey:AccountID;references:id"`
DNSSettings DNSSettings `gorm:"embedded;embeddedPrefix:dns_settings_"`
PostureChecks []*posture.Checks `gorm:"foreignKey:AccountID;references:id"`
ReverseProxies []*reverseproxy.ReverseProxy `gorm:"foreignKey:AccountID;references:id"`
// Settings is a dictionary of Account settings
Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"`
Networks []*networkTypes.Network `gorm:"foreignKey:AccountID;references:id"`
@@ -283,6 +285,8 @@ func (a *Account) GetPeerNetworkMap(
routers map[string]map[string]*routerTypes.NetworkRouter,
metrics *telemetry.AccountManagerMetrics,
groupIDToUserIDs map[string][]string,
exposedServices map[string][]*reverseproxy.ReverseProxy, // routerPeer -> list of exposed services
proxyPeers []*nbpeer.Peer,
) *NetworkMap {
start := time.Now()
peer := a.Peers[peerID]
@@ -300,10 +304,21 @@ func (a *Account) GetPeerNetworkMap(
peerGroups := a.GetPeerGroups(peerID)
aclPeers, firewallRules, authorizedUsers, enableSSH := a.GetPeerConnectionResources(ctx, peer, validatedPeersMap, groupIDToUserIDs)
var aclPeers []*nbpeer.Peer
var firewallRules []*FirewallRule
var authorizedUsers map[string]map[string]struct{}
var enableSSH bool
if peer.ProxyEmbedded {
aclPeers, firewallRules = a.GetProxyConnectionResources(exposedServices)
} else {
aclPeers, firewallRules, authorizedUsers, enableSSH = a.GetPeerConnectionResources(ctx, peer, validatedPeersMap, groupIDToUserIDs)
proxyAclPeers, proxyFirewallRules := a.GetPeerProxyResources(exposedServices[peerID], proxyPeers)
aclPeers = append(aclPeers, proxyAclPeers...)
firewallRules = append(firewallRules, proxyFirewallRules...)
}
var peersToConnect, expiredPeers []*nbpeer.Peer
// exclude expired peers
var peersToConnect []*nbpeer.Peer
var expiredPeers []*nbpeer.Peer
for _, p := range aclPeers {
expired, _ := p.LoginExpired(a.Settings.PeerLoginExpiration)
if a.Settings.PeerLoginExpirationEnabled && expired {
@@ -372,6 +387,74 @@ func (a *Account) GetPeerNetworkMap(
return nm
}
func (a *Account) GetProxyConnectionResources(exposedServices map[string][]*reverseproxy.ReverseProxy) ([]*nbpeer.Peer, []*FirewallRule) {
var aclPeers []*nbpeer.Peer
var firewallRules []*FirewallRule
for _, peerServices := range exposedServices {
for _, service := range peerServices {
if !service.Enabled {
continue
}
for _, target := range service.Targets {
if !target.Enabled {
continue
}
switch target.TargetType {
case reverseproxy.TargetTypePeer:
tpeer := a.GetPeer(target.TargetId)
if tpeer == nil {
continue
}
aclPeers = append(aclPeers, tpeer)
firewallRules = append(firewallRules, &FirewallRule{
PolicyID: "proxy-" + service.ID,
PeerIP: tpeer.IP.String(),
Direction: FirewallRuleDirectionOUT,
Action: "allow",
Protocol: string(PolicyRuleProtocolTCP),
PortRange: RulePortRange{Start: uint16(target.Port), End: uint16(target.Port)},
})
case reverseproxy.TargetTypeResource:
// TODO: handle resource type targets
}
}
}
}
return aclPeers, firewallRules
}
func (a *Account) GetPeerProxyResources(services []*reverseproxy.ReverseProxy, proxyPeers []*nbpeer.Peer) ([]*nbpeer.Peer, []*FirewallRule) {
var aclPeers []*nbpeer.Peer
var firewallRules []*FirewallRule
for _, service := range services {
if !service.Enabled {
continue
}
for _, target := range service.Targets {
if !target.Enabled {
continue
}
aclPeers = proxyPeers
for _, peer := range aclPeers {
firewallRules = append(firewallRules, &FirewallRule{
PolicyID: "proxy-" + service.ID,
PeerIP: peer.IP.String(),
Direction: FirewallRuleDirectionIN,
Action: "allow",
Protocol: string(PolicyRuleProtocolTCP),
PortRange: RulePortRange{Start: uint16(target.Port), End: uint16(target.Port)},
})
}
// TODO: handle routes
}
}
return aclPeers, firewallRules
}
func (a *Account) addNetworksRoutingPeers(
networkResourcesRoutes []*route.Route,
peer *nbpeer.Peer,
@@ -1215,7 +1298,7 @@ func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, pe
filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs))
for _, p := range uniquePeerIDs {
peer, ok := a.Peers[p]
if !ok || peer == nil {
if !ok || peer == nil || peer.ProxyEmbedded {
continue
}
@@ -1242,7 +1325,7 @@ func (a *Account) getAllPeersFromGroups(ctx context.Context, groups []string, pe
func (a *Account) getPeerFromResource(resource Resource, peerID string) ([]*nbpeer.Peer, bool) {
peer := a.GetPeer(resource.ID)
if peer == nil {
if peer == nil || peer.ProxyEmbedded {
return []*nbpeer.Peer{}, false
}
@@ -1778,6 +1861,40 @@ func (a *Account) GetActiveGroupUsers() map[string][]string {
return groups
}
func (a *Account) GetProxyPeers() []*nbpeer.Peer {
var proxyPeers []*nbpeer.Peer
for _, peer := range a.Peers {
if peer.ProxyEmbedded {
proxyPeers = append(proxyPeers, peer)
}
}
return proxyPeers
}
func (a *Account) GetExposedServicesMap() map[string][]*reverseproxy.ReverseProxy {
services := make(map[string][]*reverseproxy.ReverseProxy)
resourcesMap := make(map[string]*resourceTypes.NetworkResource)
for _, resource := range a.NetworkResources {
resourcesMap[resource.ID] = resource
}
routersMap := a.GetResourceRoutersMap()
for _, proxy := range a.ReverseProxies {
for _, target := range proxy.Targets {
switch target.TargetType {
case reverseproxy.TargetTypePeer:
services[target.TargetId] = append(services[target.TargetId], proxy)
case reverseproxy.TargetTypeResource:
resource := resourcesMap[target.TargetId]
routers := routersMap[resource.NetworkID]
for peerID := range routers {
services[peerID] = append(services[peerID], proxy)
}
}
}
}
return services
}
// expandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules
func expandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule {
features := peerSupportedFirewallFeatures(peer.Meta.WtVersion)