mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-18 22:59:57 +00:00
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:
@@ -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()))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
143
management/server/http/handlers/proxy/auth.go
Normal file
143
management/server/http/handlers/proxy/auth.go
Normal 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 ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user