mirror of
https://github.com/netbirdio/netbird.git
synced 2026-03-31 06:24:18 -04:00
[management, proxy] Add require_subdomain capability for proxy clusters (#5628)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -35,4 +35,5 @@ type Controller interface {
|
||||
UnregisterProxyFromCluster(ctx context.Context, clusterAddr, proxyID string) error
|
||||
GetProxiesForCluster(clusterAddr string) []string
|
||||
ClusterSupportsCustomPorts(clusterAddr string) *bool
|
||||
ClusterRequireSubdomain(clusterAddr string) *bool
|
||||
}
|
||||
|
||||
@@ -77,6 +77,12 @@ func (c *GRPCController) ClusterSupportsCustomPorts(clusterAddr string) *bool {
|
||||
return c.proxyGRPCServer.ClusterSupportsCustomPorts(clusterAddr)
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain returns whether the cluster requires a subdomain label.
|
||||
// Returns nil when no proxy has reported the capability (defaults to false).
|
||||
func (c *GRPCController) ClusterRequireSubdomain(clusterAddr string) *bool {
|
||||
return c.proxyGRPCServer.ClusterRequireSubdomain(clusterAddr)
|
||||
}
|
||||
|
||||
// GetProxiesForCluster returns all proxy IDs registered for a specific cluster.
|
||||
func (c *GRPCController) GetProxiesForCluster(clusterAddr string) []string {
|
||||
proxySet, ok := c.clusterProxies.Load(clusterAddr)
|
||||
|
||||
@@ -159,6 +159,20 @@ func (mr *MockControllerMockRecorder) ClusterSupportsCustomPorts(clusterAddr int
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSupportsCustomPorts", reflect.TypeOf((*MockController)(nil).ClusterSupportsCustomPorts), clusterAddr)
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain mocks base method.
|
||||
func (m *MockController) ClusterRequireSubdomain(clusterAddr string) *bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ClusterRequireSubdomain", clusterAddr)
|
||||
ret0, _ := ret[0].(*bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain indicates an expected call of ClusterRequireSubdomain.
|
||||
func (mr *MockControllerMockRecorder) ClusterRequireSubdomain(clusterAddr interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterRequireSubdomain", reflect.TypeOf((*MockController)(nil).ClusterRequireSubdomain), clusterAddr)
|
||||
}
|
||||
|
||||
// GetOIDCValidationConfig mocks base method.
|
||||
func (m *MockController) GetOIDCValidationConfig() OIDCValidationConfig {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -76,6 +76,7 @@ func setupL4Test(t *testing.T, customPortsSupported *bool) (*Manager, store.Stor
|
||||
|
||||
mockCtrl := proxy.NewMockController(ctrl)
|
||||
mockCtrl.EXPECT().ClusterSupportsCustomPorts(gomock.Any()).Return(customPortsSupported).AnyTimes()
|
||||
mockCtrl.EXPECT().ClusterRequireSubdomain(gomock.Any()).Return((*bool)(nil)).AnyTimes()
|
||||
mockCtrl.EXPECT().SendServiceUpdateToCluster(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||
mockCtrl.EXPECT().GetOIDCValidationConfig().Return(proxy.OIDCValidationConfig{}).AnyTimes()
|
||||
|
||||
|
||||
@@ -236,6 +236,10 @@ func (m *Manager) initializeServiceForCreate(ctx context.Context, accountID stri
|
||||
return status.Errorf(status.PreconditionFailed, "could not derive cluster from domain %s: %v", service.Domain, err)
|
||||
}
|
||||
service.ProxyCluster = proxyCluster
|
||||
|
||||
if err := m.validateSubdomainRequirement(service.Domain, proxyCluster); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
service.AccountID = accountID
|
||||
@@ -261,6 +265,20 @@ func (m *Manager) initializeServiceForCreate(ctx context.Context, accountID stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateSubdomainRequirement checks whether the domain can be used bare
|
||||
// (without a subdomain label) on the given cluster. If the cluster reports
|
||||
// require_subdomain=true and the domain equals the cluster domain, it rejects.
|
||||
func (m *Manager) validateSubdomainRequirement(domain, cluster string) error {
|
||||
if domain != cluster {
|
||||
return nil
|
||||
}
|
||||
requireSub := m.proxyController.ClusterRequireSubdomain(cluster)
|
||||
if requireSub != nil && *requireSub {
|
||||
return status.Errorf(status.InvalidArgument, "domain %s requires a subdomain label", domain)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) persistNewService(ctx context.Context, accountID string, svc *service.Service) error {
|
||||
return m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
if svc.Domain != "" {
|
||||
@@ -489,53 +507,61 @@ func (m *Manager) persistServiceUpdate(ctx context.Context, accountID string, se
|
||||
var updateInfo serviceUpdateInfo
|
||||
|
||||
err := m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||
existingService, err := transaction.GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, service.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateProtocolChange(existingService.Mode, service.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateInfo.oldCluster = existingService.ProxyCluster
|
||||
updateInfo.domainChanged = existingService.Domain != service.Domain
|
||||
|
||||
if updateInfo.domainChanged {
|
||||
if err := m.handleDomainChange(ctx, transaction, accountID, service); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
service.ProxyCluster = existingService.ProxyCluster
|
||||
}
|
||||
|
||||
m.preserveExistingAuthSecrets(service, existingService)
|
||||
if err := validateHeaderAuthValues(service.Auth.HeaderAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
m.preserveServiceMetadata(service, existingService)
|
||||
m.preserveListenPort(service, existingService)
|
||||
updateInfo.serviceEnabledChanged = existingService.Enabled != service.Enabled
|
||||
|
||||
if err := m.ensureL4Port(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.checkPortConflict(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTargetReferences(ctx, transaction, accountID, service.Targets); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := transaction.UpdateService(ctx, service); err != nil {
|
||||
return fmt.Errorf("update service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return m.executeServiceUpdate(ctx, transaction, accountID, service, &updateInfo)
|
||||
})
|
||||
|
||||
return &updateInfo, err
|
||||
}
|
||||
|
||||
func (m *Manager) executeServiceUpdate(ctx context.Context, transaction store.Store, accountID string, service *service.Service, updateInfo *serviceUpdateInfo) error {
|
||||
existingService, err := transaction.GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, service.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateProtocolChange(existingService.Mode, service.Mode); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateInfo.oldCluster = existingService.ProxyCluster
|
||||
updateInfo.domainChanged = existingService.Domain != service.Domain
|
||||
|
||||
if updateInfo.domainChanged {
|
||||
if err := m.handleDomainChange(ctx, transaction, accountID, service); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
service.ProxyCluster = existingService.ProxyCluster
|
||||
}
|
||||
|
||||
if err := m.validateSubdomainRequirement(service.Domain, service.ProxyCluster); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.preserveExistingAuthSecrets(service, existingService)
|
||||
if err := validateHeaderAuthValues(service.Auth.HeaderAuths); err != nil {
|
||||
return err
|
||||
}
|
||||
m.preserveServiceMetadata(service, existingService)
|
||||
m.preserveListenPort(service, existingService)
|
||||
updateInfo.serviceEnabledChanged = existingService.Enabled != service.Enabled
|
||||
|
||||
if err := m.ensureL4Port(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.checkPortConflict(ctx, transaction, service); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTargetReferences(ctx, transaction, accountID, service.Targets); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := transaction.UpdateService(ctx, service); err != nil {
|
||||
return fmt.Errorf("update service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleDomainChange(ctx context.Context, transaction store.Store, accountID string, svc *service.Service) error {
|
||||
if err := m.checkDomainAvailable(ctx, transaction, svc.Domain, svc.ID); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1272,3 +1272,69 @@ func TestValidateTargetReferences_PeerValid(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, validateTargetReferences(ctx, mockStore, accountID, targets))
|
||||
}
|
||||
|
||||
func TestValidateSubdomainRequirement(t *testing.T) {
|
||||
ptrBool := func(b bool) *bool { return &b }
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
domain string
|
||||
cluster string
|
||||
requireSubdomain *bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "subdomain present, require_subdomain true",
|
||||
domain: "app.eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(true),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain, require_subdomain true",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(true),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain, require_subdomain false",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(false),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "bare cluster domain, require_subdomain nil (default)",
|
||||
domain: "eu1.proxy.netbird.io",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "custom domain apex is not the cluster",
|
||||
domain: "example.com",
|
||||
cluster: "eu1.proxy.netbird.io",
|
||||
requireSubdomain: ptrBool(true),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
mockCtrl := proxy.NewMockController(ctrl)
|
||||
mockCtrl.EXPECT().ClusterRequireSubdomain(tc.cluster).Return(tc.requireSubdomain).AnyTimes()
|
||||
|
||||
mgr := &Manager{proxyController: mockCtrl}
|
||||
err := mgr.validateSubdomainRequirement(tc.domain, tc.cluster)
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "requires a subdomain label")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,6 +537,35 @@ func (s *ProxyServiceServer) ClusterSupportsCustomPorts(clusterAddr string) *boo
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClusterRequireSubdomain returns whether any connected proxy in the given
|
||||
// cluster reports that a subdomain is required. Returns nil if no proxy has
|
||||
// reported the capability (defaults to not required).
|
||||
func (s *ProxyServiceServer) ClusterRequireSubdomain(clusterAddr string) *bool {
|
||||
if s.proxyController == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hasCapabilities bool
|
||||
for _, pid := range s.proxyController.GetProxiesForCluster(clusterAddr) {
|
||||
connVal, ok := s.connectedProxies.Load(pid)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
conn := connVal.(*proxyConnection)
|
||||
if conn.capabilities == nil || conn.capabilities.RequireSubdomain == nil {
|
||||
continue
|
||||
}
|
||||
if *conn.capabilities.RequireSubdomain {
|
||||
return ptr(true)
|
||||
}
|
||||
hasCapabilities = true
|
||||
}
|
||||
if hasCapabilities {
|
||||
return ptr(false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ProxyServiceServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateResponse, error) {
|
||||
service, err := s.serviceManager.GetServiceByID(ctx, req.GetAccountId(), req.GetId())
|
||||
if err != nil {
|
||||
|
||||
@@ -57,6 +57,10 @@ func (c *testProxyController) ClusterSupportsCustomPorts(_ string) *bool {
|
||||
return ptr(true)
|
||||
}
|
||||
|
||||
func (c *testProxyController) ClusterRequireSubdomain(_ string) *bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testProxyController) GetProxiesForCluster(clusterAddr string) []string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -62,6 +62,7 @@ var (
|
||||
proxyProtocol bool
|
||||
preSharedKey string
|
||||
supportsCustomPorts bool
|
||||
requireSubdomain bool
|
||||
geoDataDir string
|
||||
)
|
||||
|
||||
@@ -101,6 +102,7 @@ func init() {
|
||||
rootCmd.Flags().BoolVar(&proxyProtocol, "proxy-protocol", envBoolOrDefault("NB_PROXY_PROXY_PROTOCOL", false), "Enable PROXY protocol on TCP listeners to preserve client IPs behind L4 proxies")
|
||||
rootCmd.Flags().StringVar(&preSharedKey, "preshared-key", envStringOrDefault("NB_PROXY_PRESHARED_KEY", ""), "Define a pre-shared key for the tunnel between proxy and peers")
|
||||
rootCmd.Flags().BoolVar(&supportsCustomPorts, "supports-custom-ports", envBoolOrDefault("NB_PROXY_SUPPORTS_CUSTOM_PORTS", true), "Whether the proxy can bind arbitrary ports for UDP/TCP passthrough")
|
||||
rootCmd.Flags().BoolVar(&requireSubdomain, "require-subdomain", envBoolOrDefault("NB_PROXY_REQUIRE_SUBDOMAIN", false), "Require a subdomain label in front of the cluster domain")
|
||||
rootCmd.Flags().DurationVar(&maxDialTimeout, "max-dial-timeout", envDurationOrDefault("NB_PROXY_MAX_DIAL_TIMEOUT", 0), "Cap per-service backend dial timeout (0 = no cap)")
|
||||
rootCmd.Flags().DurationVar(&maxSessionIdleTimeout, "max-session-idle-timeout", envDurationOrDefault("NB_PROXY_MAX_SESSION_IDLE_TIMEOUT", 0), "Cap per-service session idle timeout (0 = no cap)")
|
||||
rootCmd.Flags().StringVar(&geoDataDir, "geo-data-dir", envStringOrDefault("NB_PROXY_GEO_DATA_DIR", "/var/lib/netbird/geolocation"), "Directory for the GeoLite2 MMDB file (auto-downloaded if missing)")
|
||||
@@ -181,6 +183,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
||||
ProxyProtocol: proxyProtocol,
|
||||
PreSharedKey: preSharedKey,
|
||||
SupportsCustomPorts: supportsCustomPorts,
|
||||
RequireSubdomain: requireSubdomain,
|
||||
MaxDialTimeout: maxDialTimeout,
|
||||
MaxSessionIdleTimeout: maxSessionIdleTimeout,
|
||||
GeoDataDir: geoDataDir,
|
||||
|
||||
@@ -251,6 +251,10 @@ func (c *testProxyController) ClusterSupportsCustomPorts(_ string) *bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *testProxyController) ClusterRequireSubdomain(_ string) *bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
// storeBackedServiceManager reads directly from the real store.
|
||||
type storeBackedServiceManager struct {
|
||||
store store.Store
|
||||
|
||||
@@ -163,6 +163,10 @@ type Server struct {
|
||||
// SupportsCustomPorts indicates whether the proxy can bind arbitrary
|
||||
// ports for TCP/UDP/TLS services.
|
||||
SupportsCustomPorts bool
|
||||
// RequireSubdomain indicates whether a subdomain label is required
|
||||
// in front of this proxy's cluster domain. When true, accounts cannot
|
||||
// create services on the bare cluster domain.
|
||||
RequireSubdomain bool
|
||||
// MaxDialTimeout caps the per-service backend dial timeout.
|
||||
// When the API sends a timeout, it is clamped to this value.
|
||||
// When the API sends no timeout, this value is used as the default.
|
||||
@@ -919,6 +923,7 @@ func (s *Server) newManagementMappingWorker(ctx context.Context, client proto.Pr
|
||||
Address: s.ProxyURL,
|
||||
Capabilities: &proto.ProxyCapabilities{
|
||||
SupportsCustomPorts: &s.SupportsCustomPorts,
|
||||
RequireSubdomain: &s.RequireSubdomain,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -3336,6 +3336,10 @@ components:
|
||||
type: boolean
|
||||
description: Whether the cluster supports binding arbitrary TCP/UDP ports
|
||||
example: true
|
||||
require_subdomain:
|
||||
type: boolean
|
||||
description: Whether a subdomain label is required in front of this domain. When true, the domain cannot be used bare.
|
||||
example: false
|
||||
required:
|
||||
- id
|
||||
- domain
|
||||
|
||||
@@ -3406,6 +3406,9 @@ type ReverseProxyDomain struct {
|
||||
// Id Domain ID
|
||||
Id string `json:"id"`
|
||||
|
||||
// RequireSubdomain Whether a subdomain label is required in front of this domain. When true, the domain cannot be used bare.
|
||||
RequireSubdomain *bool `json:"require_subdomain,omitempty"`
|
||||
|
||||
// SupportsCustomPorts Whether the cluster supports binding arbitrary TCP/UDP ports
|
||||
SupportsCustomPorts *bool `json:"supports_custom_ports,omitempty"`
|
||||
|
||||
|
||||
@@ -181,8 +181,11 @@ type ProxyCapabilities struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Whether the proxy can bind arbitrary ports for TCP/UDP/TLS services.
|
||||
SupportsCustomPorts *bool `protobuf:"varint,1,opt,name=supports_custom_ports,json=supportsCustomPorts,proto3,oneof" json:"supports_custom_ports,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// Whether the proxy requires a subdomain label in front of its cluster domain.
|
||||
// When true, tenants cannot use the cluster domain bare.
|
||||
RequireSubdomain *bool `protobuf:"varint,2,opt,name=require_subdomain,json=requireSubdomain,proto3,oneof" json:"require_subdomain,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ProxyCapabilities) Reset() {
|
||||
@@ -222,6 +225,13 @@ func (x *ProxyCapabilities) GetSupportsCustomPorts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *ProxyCapabilities) GetRequireSubdomain() bool {
|
||||
if x != nil && x.RequireSubdomain != nil {
|
||||
return *x.RequireSubdomain
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetMappingUpdateRequest is sent to initialise a mapping stream.
|
||||
type GetMappingUpdateRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@@ -1872,10 +1882,12 @@ var File_proxy_service_proto protoreflect.FileDescriptor
|
||||
const file_proxy_service_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x13proxy_service.proto\x12\n" +
|
||||
"management\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"f\n" +
|
||||
"management\x1a\x1egoogle/protobuf/duration.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xae\x01\n" +
|
||||
"\x11ProxyCapabilities\x127\n" +
|
||||
"\x15supports_custom_ports\x18\x01 \x01(\bH\x00R\x13supportsCustomPorts\x88\x01\x01B\x18\n" +
|
||||
"\x16_supports_custom_ports\"\xe6\x01\n" +
|
||||
"\x15supports_custom_ports\x18\x01 \x01(\bH\x00R\x13supportsCustomPorts\x88\x01\x01\x120\n" +
|
||||
"\x11require_subdomain\x18\x02 \x01(\bH\x01R\x10requireSubdomain\x88\x01\x01B\x18\n" +
|
||||
"\x16_supports_custom_portsB\x14\n" +
|
||||
"\x12_require_subdomain\"\xe6\x01\n" +
|
||||
"\x17GetMappingUpdateRequest\x12\x19\n" +
|
||||
"\bproxy_id\x18\x01 \x01(\tR\aproxyId\x12\x18\n" +
|
||||
"\aversion\x18\x02 \x01(\tR\aversion\x129\n" +
|
||||
|
||||
@@ -31,6 +31,9 @@ service ProxyService {
|
||||
message ProxyCapabilities {
|
||||
// Whether the proxy can bind arbitrary ports for TCP/UDP/TLS services.
|
||||
optional bool supports_custom_ports = 1;
|
||||
// Whether the proxy requires a subdomain label in front of its cluster domain.
|
||||
// When true, accounts cannot use the cluster domain bare.
|
||||
optional bool require_subdomain = 2;
|
||||
}
|
||||
|
||||
// GetMappingUpdateRequest is sent to initialise a mapping stream.
|
||||
|
||||
Reference in New Issue
Block a user