diff --git a/management/internals/modules/reverseproxy/domain/api.go b/management/internals/modules/reverseproxy/domain/api.go index 7d1a37cc8..87b6a4f26 100644 --- a/management/internals/modules/reverseproxy/domain/api.go +++ b/management/internals/modules/reverseproxy/domain/api.go @@ -5,7 +5,6 @@ import ( "net/http" "github.com/gorilla/mux" - nbcontext "github.com/netbirdio/netbird/management/server/context" "github.com/netbirdio/netbird/shared/management/http/api" "github.com/netbirdio/netbird/shared/management/http/util" @@ -40,12 +39,16 @@ func domainTypeToApi(t domainType) api.ReverseProxyDomainType { } func domainToApi(d *Domain) api.ReverseProxyDomain { - return api.ReverseProxyDomain{ + resp := api.ReverseProxyDomain{ Domain: d.Domain, Id: d.ID, Type: domainTypeToApi(d.Type), Validated: d.Validated, } + if d.TargetCluster != "" { + resp.TargetCluster = &d.TargetCluster + } + return resp } func (h *handler) getAllDomains(w http.ResponseWriter, r *http.Request) { @@ -82,7 +85,7 @@ func (h *handler) createCustomDomain(w http.ResponseWriter, r *http.Request) { return } - domain, err := h.manager.CreateDomain(r.Context(), userAuth.AccountId, req.Domain) + domain, err := h.manager.CreateDomain(r.Context(), userAuth.AccountId, req.Domain, req.TargetCluster) if err != nil { util.WriteError(r.Context(), err, w) return diff --git a/management/internals/modules/reverseproxy/domain/manager.go b/management/internals/modules/reverseproxy/domain/manager.go index 8d0498706..b54365f91 100644 --- a/management/internals/modules/reverseproxy/domain/manager.go +++ b/management/internals/modules/reverseproxy/domain/manager.go @@ -19,11 +19,12 @@ const ( ) type Domain struct { - ID string `gorm:"unique;primaryKey;autoIncrement"` - Domain string `gorm:"unique"` // Domain records must be unique, this avoids domain reuse across accounts. - AccountID string `gorm:"index"` - Type domainType `gorm:"-"` - Validated bool + ID string `gorm:"unique;primaryKey;autoIncrement"` + Domain string `gorm:"unique"` // Domain records must be unique, this avoids domain reuse across accounts. + AccountID string `gorm:"index"` + TargetCluster string // The proxy cluster this domain should be validated against + Type domainType `gorm:"-"` + Validated bool } type store interface { @@ -32,7 +33,7 @@ type store interface { GetCustomDomain(ctx context.Context, accountID string, domainID string) (*Domain, error) ListFreeDomains(ctx context.Context, accountID string) ([]string, error) ListCustomDomains(ctx context.Context, accountID string) ([]*Domain, error) - CreateCustomDomain(ctx context.Context, accountID string, domainName string, validated bool) (*Domain, error) + CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*Domain, error) UpdateCustomDomain(ctx context.Context, accountID string, d *Domain) (*Domain, error) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error } @@ -85,31 +86,43 @@ func (m Manager) GetDomains(ctx context.Context, accountID string) ([]*Domain, e // Add custom domains. for _, domain := range domains { ret = append(ret, &Domain{ - ID: domain.ID, - Domain: domain.Domain, - AccountID: accountID, - Type: TypeCustom, - Validated: domain.Validated, + ID: domain.ID, + Domain: domain.Domain, + AccountID: accountID, + TargetCluster: domain.TargetCluster, + Type: TypeCustom, + Validated: domain.Validated, }) } return ret, nil } -func (m Manager) CreateDomain(ctx context.Context, accountID, domainName string) (*Domain, error) { - // Attempt an initial validation; however, a failure is still acceptable for creation - // because the user may not yet have configured their DNS records, or the DNS update - // has not yet reached the servers that are queried by the validation resolver. +func (m Manager) CreateDomain(ctx context.Context, accountID, domainName, targetCluster string) (*Domain, error) { + + // Verify the target cluster is in the available clusters + allowList := m.proxyURLAllowList() + clusterValid := false + for _, cluster := range allowList { + if cluster == targetCluster { + clusterValid = true + break + } + } + if !clusterValid { + return nil, fmt.Errorf("target cluster %s is not available", targetCluster) + } + + // Attempt an initial validation against the specified cluster only var validated bool - if m.validator.IsValid(ctx, domainName, m.proxyURLAllowList()) { + if m.validator.IsValid(ctx, domainName, []string{targetCluster}) { validated = true } - d, err := m.store.CreateCustomDomain(ctx, accountID, domainName, validated) + d, err := m.store.CreateCustomDomain(ctx, accountID, domainName, targetCluster, validated) if err != nil { return d, fmt.Errorf("create domain in store: %w", err) } - return d, nil } @@ -136,15 +149,25 @@ func (m Manager) ValidateDomain(accountID, domainID string) { return } - allowList := m.proxyURLAllowList() - log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - "domain": d.Domain, - "proxyAllowList": allowList, - }).Info("validating domain against proxy allow list") + // Validate only against the domain's target cluster + targetCluster := d.TargetCluster + if targetCluster == "" { + log.WithFields(log.Fields{ + "accountID": accountID, + "domainID": domainID, + "domain": d.Domain, + }).Warn("domain has no target cluster set, skipping validation") + return + } - if m.validator.IsValid(context.Background(), d.Domain, allowList) { + log.WithFields(log.Fields{ + "accountID": accountID, + "domainID": domainID, + "domain": d.Domain, + "targetCluster": targetCluster, + }).Info("validating domain against target cluster") + + if m.validator.IsValid(context.Background(), d.Domain, []string{targetCluster}) { log.WithFields(log.Fields{ "accountID": accountID, "domainID": domainID, @@ -161,11 +184,11 @@ func (m Manager) ValidateDomain(accountID, domainID string) { } } else { log.WithFields(log.Fields{ - "accountID": accountID, - "domainID": domainID, - "domain": d.Domain, - "proxyAllowList": allowList, - }).Warn("domain validation failed - CNAME does not match any connected proxy") + "accountID": accountID, + "domainID": domainID, + "domain": d.Domain, + "targetCluster": targetCluster, + }).Warn("domain validation failed - CNAME does not match target cluster") } } diff --git a/management/server/store/sql_store.go b/management/server/store/sql_store.go index 0dadb6921..b82739a06 100644 --- a/management/server/store/sql_store.go +++ b/management/server/store/sql_store.go @@ -4845,13 +4845,14 @@ func (s *SqlStore) ListCustomDomains(ctx context.Context, accountID string) ([]* return domains, nil } -func (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, domainName string, validated bool) (*domain.Domain, error) { +func (s *SqlStore) CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error) { newDomain := &domain.Domain{ - ID: xid.New().String(), // Generate our own ID because gorm doesn't always configure the database to handle this for us. - Domain: domainName, - AccountID: accountID, - Type: domain.TypeCustom, - Validated: validated, + ID: xid.New().String(), // Generate our own ID because gorm doesn't always configure the database to handle this for us. + Domain: domainName, + AccountID: accountID, + TargetCluster: targetCluster, + Type: domain.TypeCustom, + Validated: validated, } result := s.db.Create(newDomain) if result.Error != nil { diff --git a/management/server/store/store.go b/management/server/store/store.go index ce27ab9e2..feb403a38 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -255,7 +255,7 @@ type Store interface { GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error) ListFreeDomains(ctx context.Context, accountID string) ([]string, error) ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error) - CreateCustomDomain(ctx context.Context, accountID string, domainName string, validated bool) (*domain.Domain, error) + CreateCustomDomain(ctx context.Context, accountID string, domainName string, targetCluster string, validated bool) (*domain.Domain, error) UpdateCustomDomain(ctx context.Context, accountID string, d *domain.Domain) (*domain.Domain, error) DeleteCustomDomain(ctx context.Context, accountID string, domainID string) error diff --git a/shared/management/http/api/openapi.yml b/shared/management/http/api/openapi.yml index c43e7351c..07e1819e9 100644 --- a/shared/management/http/api/openapi.yml +++ b/shared/management/http/api/openapi.yml @@ -3047,6 +3047,9 @@ components: description: Whether the domain has been validated type: $ref: '#/components/schemas/ReverseProxyDomainType' + target_cluster: + type: string + description: The proxy cluster this domain is validated against (only for custom domains) required: - id - domain @@ -3058,8 +3061,12 @@ components: domain: type: string description: Domain name + target_cluster: + type: string + description: The proxy cluster this domain should be validated against required: - domain + - target_cluster InstanceStatus: type: object description: Instance status information diff --git a/shared/management/http/api/types.gen.go b/shared/management/http/api/types.gen.go index 0d56bcca4..0ea5e9e79 100644 --- a/shared/management/http/api/types.gen.go +++ b/shared/management/http/api/types.gen.go @@ -2006,6 +2006,9 @@ type ReverseProxyDomain struct { // Id Domain ID Id string `json:"id"` + // TargetCluster The proxy cluster this domain is validated against (only for custom domains) + TargetCluster *string `json:"target_cluster,omitempty"` + // Type Type of Reverse Proxy Domain Type ReverseProxyDomainType `json:"type"` @@ -2017,6 +2020,9 @@ type ReverseProxyDomain struct { type ReverseProxyDomainRequest struct { // Domain Domain name Domain string `json:"domain"` + + // TargetCluster The proxy cluster this domain should be validated against + TargetCluster string `json:"target_cluster"` } // ReverseProxyDomainType Type of Reverse Proxy Domain