diff --git a/docs/documentation/configuration/overview.md b/docs/documentation/configuration/overview.md index 70496aa..8853af2 100644 --- a/docs/documentation/configuration/overview.md +++ b/docs/documentation/configuration/overview.md @@ -38,6 +38,7 @@ advanced: rule_prio_offset: 20000 route_table_offset: 20000 api_admin_only: true + limit_additional_user_peers: 0 database: debug: false @@ -215,6 +216,10 @@ Additional or more specialized configuration options for logging and interface c - **Default:** `true` - **Description:** If `true`, the public REST API is accessible only to admin users. The API docs live at [`/api/v1/doc.html`](../rest-api/api-doc.md). +### `limit_additional_user_peers` +- **Default:** `0` +- **Description:** Limit additional peers a normal user can create. `0` means unlimited. + --- ## Database diff --git a/internal/app/wireguard/wireguard_peers.go b/internal/app/wireguard/wireguard_peers.go index 2131323..1406e85 100644 --- a/internal/app/wireguard/wireguard_peers.go +++ b/internal/app/wireguard/wireguard_peers.go @@ -188,6 +188,30 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee sessionUser := domain.GetUserInfo(ctx) + // Enforce peer limit for non-admin users if LimitAdditionalUserPeers is set + if m.cfg.Core.SelfProvisioningAllowed && !sessionUser.IsAdmin && m.cfg.Advanced.LimitAdditionalUserPeers > 0 { + peers, err := m.db.GetUserPeers(ctx, peer.UserIdentifier) + if err != nil { + return nil, fmt.Errorf("failed to fetch peers for user %s: %w", peer.UserIdentifier, err) + } + // Count enabled peers (disabled IS NULL) + peerCount := 0 + for _, p := range peers { + if !p.IsDisabled() { + peerCount++ + } + } + totalAllowedPeers := 1 + m.cfg.Advanced.LimitAdditionalUserPeers // 1 default peer + x additional peers + if peerCount >= totalAllowedPeers { + slog.WarnContext(ctx, "peer creation blocked due to limit", + "user", peer.UserIdentifier, + "current_count", peerCount, + "allowed_count", totalAllowedPeers) + return nil, fmt.Errorf("peer limit reached (%d peers allowed): %w", totalAllowedPeers, domain.ErrNoPermission) + } + } + + existingPeer, err := m.db.GetPeer(ctx, peer.Identifier) if err != nil && !errors.Is(err, domain.ErrNotFound) { return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err) diff --git a/internal/config/config.go b/internal/config/config.go index f8ade2f..66ff746 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,18 +29,19 @@ type Config struct { } `yaml:"core"` Advanced struct { - LogLevel string `yaml:"log_level"` - LogPretty bool `yaml:"log_pretty"` - LogJson bool `yaml:"log_json"` - StartListenPort int `yaml:"start_listen_port"` - StartCidrV4 string `yaml:"start_cidr_v4"` - StartCidrV6 string `yaml:"start_cidr_v6"` - UseIpV6 bool `yaml:"use_ip_v6"` - ConfigStoragePath string `yaml:"config_storage_path"` // keep empty to disable config export to file - ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"` - RulePrioOffset int `yaml:"rule_prio_offset"` - RouteTableOffset int `yaml:"route_table_offset"` - ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API + LogLevel string `yaml:"log_level"` + LogPretty bool `yaml:"log_pretty"` + LogJson bool `yaml:"log_json"` + StartListenPort int `yaml:"start_listen_port"` + StartCidrV4 string `yaml:"start_cidr_v4"` + StartCidrV6 string `yaml:"start_cidr_v6"` + UseIpV6 bool `yaml:"use_ip_v6"` + ConfigStoragePath string `yaml:"config_storage_path"` // keep empty to disable config export to file + ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"` + RulePrioOffset int `yaml:"rule_prio_offset"` + RouteTableOffset int `yaml:"route_table_offset"` + ApiAdminOnly bool `yaml:"api_admin_only"` // if true, only admin users can access the API + LimitAdditionalUserPeers int `yaml:"limit_additional_user_peers"` } `yaml:"advanced"` Statistics struct { @@ -76,6 +77,7 @@ func (c *Config) LogStartupValues() { "reEnablePeerAfterUserEnable", c.Core.ReEnablePeerAfterUserEnable, "deletePeerAfterUserDeleted", c.Core.DeletePeerAfterUserDeleted, "selfProvisioningAllowed", c.Core.SelfProvisioningAllowed, + "limitAdditionalUserPeers", c.Advanced.LimitAdditionalUserPeers, "importExisting", c.Core.ImportExisting, "restoreState", c.Core.RestoreState, "useIpV6", c.Advanced.UseIpV6, @@ -137,6 +139,7 @@ func defaultConfig() *Config { cfg.Advanced.RulePrioOffset = 20000 cfg.Advanced.RouteTableOffset = 20000 cfg.Advanced.ApiAdminOnly = true + cfg.Advanced.LimitAdditionalUserPeers = 0 cfg.Statistics.UsePingChecks = true cfg.Statistics.PingCheckWorkers = 10