[management, proxy] Add require_subdomain capability for proxy clusters (#5628)

This commit is contained in:
Viktor Liu
2026-03-20 18:29:50 +08:00
committed by GitHub
parent ab77508950
commit b550a2face
19 changed files with 419 additions and 52 deletions

View File

@@ -17,6 +17,9 @@ type Domain struct {
// SupportsCustomPorts is populated at query time for free domains from the
// proxy cluster capabilities. Not persisted.
SupportsCustomPorts *bool `gorm:"-"`
// RequireSubdomain is populated at query time. When true, the domain
// cannot be used bare and a subdomain label must be prepended. Not persisted.
RequireSubdomain *bool `gorm:"-"`
}
// EventMeta returns activity event metadata for a domain

View File

@@ -47,6 +47,7 @@ func domainToApi(d *domain.Domain) api.ReverseProxyDomain {
Type: domainTypeToApi(d.Type),
Validated: d.Validated,
SupportsCustomPorts: d.SupportsCustomPorts,
RequireSubdomain: d.RequireSubdomain,
}
if d.TargetCluster != "" {
resp.TargetCluster = &d.TargetCluster

View File

@@ -0,0 +1,172 @@
package manager
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain"
)
func TestExtractClusterFromFreeDomain(t *testing.T) {
clusters := []string{"eu1.proxy.netbird.io", "us1.proxy.netbird.io"}
tests := []struct {
name string
domain string
wantOK bool
wantVal string
}{
{
name: "subdomain of cluster matches",
domain: "myapp.eu1.proxy.netbird.io",
wantOK: true,
wantVal: "eu1.proxy.netbird.io",
},
{
name: "deep subdomain of cluster matches",
domain: "foo.bar.eu1.proxy.netbird.io",
wantOK: true,
wantVal: "eu1.proxy.netbird.io",
},
{
name: "bare cluster domain matches",
domain: "eu1.proxy.netbird.io",
wantOK: true,
wantVal: "eu1.proxy.netbird.io",
},
{
name: "unrelated domain does not match",
domain: "example.com",
wantOK: false,
},
{
name: "partial suffix does not match",
domain: "fakeu1.proxy.netbird.io",
wantOK: false,
},
{
name: "second cluster matches",
domain: "app.us1.proxy.netbird.io",
wantOK: true,
wantVal: "us1.proxy.netbird.io",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cluster, ok := ExtractClusterFromFreeDomain(tc.domain, clusters)
assert.Equal(t, tc.wantOK, ok)
if ok {
assert.Equal(t, tc.wantVal, cluster)
}
})
}
}
func TestExtractClusterFromCustomDomains(t *testing.T) {
customDomains := []*domain.Domain{
{Domain: "example.com", TargetCluster: "eu1.proxy.netbird.io"},
{Domain: "proxy.corp.io", TargetCluster: "us1.proxy.netbird.io"},
}
tests := []struct {
name string
domain string
wantOK bool
wantVal string
}{
{
name: "subdomain of custom domain matches",
domain: "app.example.com",
wantOK: true,
wantVal: "eu1.proxy.netbird.io",
},
{
name: "bare custom domain matches",
domain: "example.com",
wantOK: true,
wantVal: "eu1.proxy.netbird.io",
},
{
name: "deep subdomain of custom domain matches",
domain: "a.b.example.com",
wantOK: true,
wantVal: "eu1.proxy.netbird.io",
},
{
name: "subdomain of multi-level custom domain matches",
domain: "app.proxy.corp.io",
wantOK: true,
wantVal: "us1.proxy.netbird.io",
},
{
name: "bare multi-level custom domain matches",
domain: "proxy.corp.io",
wantOK: true,
wantVal: "us1.proxy.netbird.io",
},
{
name: "unrelated domain does not match",
domain: "other.com",
wantOK: false,
},
{
name: "partial suffix does not match custom domain",
domain: "fakeexample.com",
wantOK: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cluster, ok := extractClusterFromCustomDomains(tc.domain, customDomains)
assert.Equal(t, tc.wantOK, ok)
if ok {
assert.Equal(t, tc.wantVal, cluster)
}
})
}
}
func TestExtractClusterFromCustomDomains_OverlappingDomains(t *testing.T) {
customDomains := []*domain.Domain{
{Domain: "example.com", TargetCluster: "cluster-generic"},
{Domain: "app.example.com", TargetCluster: "cluster-app"},
}
tests := []struct {
name string
domain string
wantVal string
}{
{
name: "exact match on more specific domain",
domain: "app.example.com",
wantVal: "cluster-app",
},
{
name: "subdomain of more specific domain",
domain: "api.app.example.com",
wantVal: "cluster-app",
},
{
name: "subdomain of generic domain",
domain: "other.example.com",
wantVal: "cluster-generic",
},
{
name: "bare generic domain",
domain: "example.com",
wantVal: "cluster-generic",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cluster, ok := extractClusterFromCustomDomains(tc.domain, customDomains)
assert.True(t, ok)
assert.Equal(t, tc.wantVal, cluster)
})
}
}

View File

@@ -35,6 +35,7 @@ type proxyManager interface {
type clusterCapabilities interface {
ClusterSupportsCustomPorts(clusterAddr string) *bool
ClusterRequireSubdomain(clusterAddr string) *bool
}
type Manager struct {
@@ -98,6 +99,7 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
}
if m.clusterCapabilities != nil {
d.SupportsCustomPorts = m.clusterCapabilities.ClusterSupportsCustomPorts(cluster)
d.RequireSubdomain = m.clusterCapabilities.ClusterRequireSubdomain(cluster)
}
ret = append(ret, d)
}
@@ -115,6 +117,8 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
if m.clusterCapabilities != nil && d.TargetCluster != "" {
cd.SupportsCustomPorts = m.clusterCapabilities.ClusterSupportsCustomPorts(d.TargetCluster)
}
// Custom domains never require a subdomain by default since
// the account owns them and should be able to use the bare domain.
ret = append(ret, cd)
}
@@ -302,13 +306,19 @@ func (m Manager) DeriveClusterFromDomain(ctx context.Context, accountID, domain
return "", fmt.Errorf("domain %s does not match any available proxy cluster", domain)
}
func extractClusterFromCustomDomains(domain string, customDomains []*domain.Domain) (string, bool) {
for _, customDomain := range customDomains {
if strings.HasSuffix(domain, "."+customDomain.Domain) {
return customDomain.TargetCluster, true
func extractClusterFromCustomDomains(serviceDomain string, customDomains []*domain.Domain) (string, bool) {
bestCluster := ""
bestLen := -1
for _, cd := range customDomains {
if serviceDomain != cd.Domain && !strings.HasSuffix(serviceDomain, "."+cd.Domain) {
continue
}
if l := len(cd.Domain); l > bestLen {
bestLen = l
bestCluster = cd.TargetCluster
}
}
return "", false
return bestCluster, bestLen >= 0
}
// ExtractClusterFromFreeDomain extracts the cluster address from a free domain.