mirror of
https://github.com/h44z/wg-portal.git
synced 2026-01-08 13:06:17 +00:00
feat: introduce "Create Default Peer" flag for interfaces (#513)
This commit is contained in:
@@ -47,7 +47,7 @@ func main() {
|
||||
rawDb, err := adapters.NewDatabase(cfg.Database)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
database, err := adapters.NewSqlRepository(rawDb)
|
||||
database, err := adapters.NewSqlRepository(rawDb, cfg)
|
||||
internal.AssertNoError(err)
|
||||
|
||||
wireGuard, err := wireguard.NewControllerManager(cfg)
|
||||
|
||||
@@ -157,12 +157,14 @@ More advanced options are found in the subsequent `Advanced` section.
|
||||
### `create_default_peer`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER`
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
- **Description:** If a user logs in for the first time with no existing peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set.
|
||||
- **Important:** This option is only effective for interfaces where the "Create default peer" flag is set (via the UI).
|
||||
|
||||
### `create_default_peer_on_creation`
|
||||
- **Default:** `false`
|
||||
- **Environment Variable:** `WG_PORTAL_CORE_CREATE_DEFAULT_PEER_ON_CREATION`
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for **all** server interfaces.
|
||||
- **Description:** If an LDAP user is created (e.g., through LDAP sync) and has no peers, automatically create a new WireGuard peer for all server interfaces where the "Create default peer" flag is set.
|
||||
- **Important:** This option requires [create_default_peer](#create_default_peer) to be enabled.
|
||||
|
||||
### `re_enable_peer_after_user_enable`
|
||||
- **Default:** `true`
|
||||
|
||||
@@ -83,6 +83,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Identifier = interfaces.Prepared.Identifier
|
||||
formData.value.DisplayName = interfaces.Prepared.DisplayName
|
||||
formData.value.Mode = interfaces.Prepared.Mode
|
||||
formData.value.CreateDefaultPeer = interfaces.Prepared.CreateDefaultPeer
|
||||
formData.value.Backend = interfaces.Prepared.Backend
|
||||
|
||||
formData.value.PublicKey = interfaces.Prepared.PublicKey
|
||||
@@ -122,6 +123,7 @@ watch(() => props.visible, async (newValue, oldValue) => {
|
||||
formData.value.Identifier = selectedInterface.value.Identifier
|
||||
formData.value.DisplayName = selectedInterface.value.DisplayName
|
||||
formData.value.Mode = selectedInterface.value.Mode
|
||||
formData.value.CreateDefaultPeer = selectedInterface.value.CreateDefaultPeer
|
||||
formData.value.Backend = selectedInterface.value.Backend
|
||||
|
||||
formData.value.PublicKey = selectedInterface.value.PublicKey
|
||||
@@ -487,6 +489,10 @@ async function del() {
|
||||
<input v-model="formData.Disabled" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.disabled.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.Mode==='server' && settings.Setting('CreateDefaultPeer')">
|
||||
<input v-model="formData.CreateDefaultPeer" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.create-default-peer.label') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch" v-if="formData.Backend==='local'">
|
||||
<input v-model="formData.SaveConfig" checked="" class="form-check-input" type="checkbox">
|
||||
<label class="form-check-label">{{ $t('modals.interface-edit.save-config.label') }}</label>
|
||||
|
||||
@@ -4,6 +4,7 @@ export function freshInterface() {
|
||||
Disabled: false,
|
||||
DisplayName: "",
|
||||
Identifier: "",
|
||||
CreateDefaultPeer: false,
|
||||
Mode: "server",
|
||||
Backend: "local",
|
||||
|
||||
|
||||
@@ -469,6 +469,9 @@
|
||||
"disabled": {
|
||||
"label": "Schnittstelle deaktiviert"
|
||||
},
|
||||
"create-default-peer": {
|
||||
"label": "Peer für neue Benutzer automatisch erstellen"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "wg-quick Konfiguration automatisch speichern"
|
||||
},
|
||||
|
||||
@@ -469,6 +469,9 @@
|
||||
"disabled": {
|
||||
"label": "Interface Disabled"
|
||||
},
|
||||
"create-default-peer": {
|
||||
"label": "Create default peer for new users"
|
||||
},
|
||||
"save-config": {
|
||||
"label": "Automatically save wg-quick config"
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
// SchemaVersion describes the current database schema version. It must be incremented if a manual migration is needed.
|
||||
var SchemaVersion uint64 = 1
|
||||
var SchemaVersion uint64 = 2
|
||||
|
||||
// SysStat stores the current database schema version and the timestamp when it was applied.
|
||||
type SysStat struct {
|
||||
@@ -179,13 +179,15 @@ func NewDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
|
||||
// SqlRepo is a SQL database repository implementation.
|
||||
// Currently, it supports MySQL, SQLite, Microsoft SQL and Postgresql database systems.
|
||||
type SqlRepo struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewSqlRepository creates a new SqlRepo instance.
|
||||
func NewSqlRepository(db *gorm.DB) (*SqlRepo, error) {
|
||||
func NewSqlRepository(db *gorm.DB, cfg *config.Config) (*SqlRepo, error) {
|
||||
repo := &SqlRepo{
|
||||
db: db,
|
||||
db: db,
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
if err := repo.preCheck(); err != nil {
|
||||
@@ -232,7 +234,9 @@ func (r *SqlRepo) migrate() error {
|
||||
slog.Debug("running migration: audit data", "result", r.db.AutoMigrate(&domain.AuditEntry{}))
|
||||
|
||||
existingSysStat := SysStat{}
|
||||
r.db.Where("schema_version = ?", SchemaVersion).First(&existingSysStat)
|
||||
r.db.Order("schema_version desc").First(&existingSysStat) // get latest version
|
||||
|
||||
// Migration: 0 --> 1
|
||||
if existingSysStat.SchemaVersion == 0 {
|
||||
sysStat := SysStat{
|
||||
MigratedAt: time.Now(),
|
||||
@@ -244,6 +248,27 @@ func (r *SqlRepo) migrate() error {
|
||||
slog.Debug("sys-stat entry written", "schema_version", SchemaVersion)
|
||||
}
|
||||
|
||||
// Migration: 1 --> 2
|
||||
if existingSysStat.SchemaVersion == 1 {
|
||||
// Preserve existing behavior for installations that had default-peer-creation enabled.
|
||||
if r.cfg.Core.CreateDefaultPeer {
|
||||
err := r.db.Model(&domain.Interface{}).
|
||||
Where("type = ?", domain.InterfaceTypeServer).
|
||||
Update("create_default_peer", true).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to migrate interface flags for schema version %d: %w", SchemaVersion, err)
|
||||
}
|
||||
slog.Debug("migrated interface create_default_peer flags", "schema_version", SchemaVersion)
|
||||
}
|
||||
sysStat := SysStat{
|
||||
MigratedAt: time.Now(),
|
||||
SchemaVersion: SchemaVersion,
|
||||
}
|
||||
if err := r.db.Create(&sysStat).Error; err != nil {
|
||||
return fmt.Errorf("failed to write sysstat entry for schema version %d: %w", SchemaVersion, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ func (e ConfigEndpoint) handleSettingsGet() http.HandlerFunc {
|
||||
MinPasswordLength: e.cfg.Auth.MinPasswordLength,
|
||||
AvailableBackends: controllerFn(),
|
||||
LoginFormVisible: !e.cfg.Auth.HideLoginForm || !hasSocialLogin,
|
||||
CreateDefaultPeer: e.cfg.Core.CreateDefaultPeer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type Settings struct {
|
||||
MinPasswordLength int `json:"MinPasswordLength"`
|
||||
AvailableBackends []SettingsBackendNames `json:"AvailableBackends"`
|
||||
LoginFormVisible bool `json:"LoginFormVisible"`
|
||||
CreateDefaultPeer bool `json:"CreateDefaultPeer"`
|
||||
}
|
||||
|
||||
type SettingsBackendNames struct {
|
||||
|
||||
@@ -9,15 +9,16 @@ import (
|
||||
)
|
||||
|
||||
type Interface struct {
|
||||
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
|
||||
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
|
||||
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
|
||||
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
|
||||
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
|
||||
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
||||
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
|
||||
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
|
||||
Identifier string `json:"Identifier" example:"wg0"` // device name, for example: wg0
|
||||
DisplayName string `json:"DisplayName"` // a nice display name/ description for the interface
|
||||
Mode string `json:"Mode" example:"server"` // the interface type, either 'server', 'client' or 'any'
|
||||
Backend string `json:"Backend" example:"local"` // the backend used for this interface e.g., local, mikrotik, ...
|
||||
PrivateKey string `json:"PrivateKey" example:"abcdef=="` // private Key of the server interface
|
||||
PublicKey string `json:"PublicKey" example:"abcdef=="` // public Key of the server interface
|
||||
Disabled bool `json:"Disabled"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string `json:"DisabledReason"` // the reason why the interface has been disabled
|
||||
SaveConfig bool `json:"SaveConfig"` // automatically persist config changes to the wgX.conf file
|
||||
CreateDefaultPeer bool `json:"CreateDefaultPeer"` // if true, default peers will be created for this interface
|
||||
|
||||
ListenPort int `json:"ListenPort"` // the listening port, for example: 51820
|
||||
Addresses []string `json:"Addresses"` // the interface ip addresses
|
||||
@@ -65,6 +66,7 @@ func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
|
||||
Disabled: src.IsDisabled(),
|
||||
DisabledReason: src.DisabledReason,
|
||||
SaveConfig: src.SaveConfig,
|
||||
CreateDefaultPeer: src.CreateDefaultPeer,
|
||||
ListenPort: src.ListenPort,
|
||||
Addresses: domain.CidrsToStringSlice(src.Addresses),
|
||||
Dns: internal.SliceString(src.DnsStr),
|
||||
@@ -151,6 +153,7 @@ func NewDomainInterface(src *Interface) *domain.Interface {
|
||||
PreDown: src.PreDown,
|
||||
PostDown: src.PostDown,
|
||||
SaveConfig: src.SaveConfig,
|
||||
CreateDefaultPeer: src.CreateDefaultPeer,
|
||||
DisplayName: src.DisplayName,
|
||||
Type: domain.InterfaceType(src.Mode),
|
||||
Backend: domain.InterfaceBackend(src.Backend),
|
||||
|
||||
@@ -374,6 +374,7 @@ func (m Manager) PrepareInterface(ctx context.Context) (*domain.Interface, error
|
||||
SaveConfig: m.cfg.Advanced.ConfigStoragePath != "",
|
||||
DisplayName: string(id),
|
||||
Type: domain.InterfaceTypeServer,
|
||||
CreateDefaultPeer: m.cfg.Core.CreateDefaultPeer,
|
||||
DriverType: "",
|
||||
Disabled: nil,
|
||||
DisabledReason: "",
|
||||
|
||||
@@ -35,6 +35,10 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
|
||||
continue // only create default peers for server interfaces
|
||||
}
|
||||
|
||||
if !iface.CreateDefaultPeer {
|
||||
continue // only create default peers if the interface flag is set
|
||||
}
|
||||
|
||||
peerAlreadyCreated := slices.ContainsFunc(userPeers, func(peer domain.Peer) bool {
|
||||
return peer.InterfaceIdentifier == iface.Identifier
|
||||
})
|
||||
|
||||
@@ -78,7 +78,12 @@ func (f *mockDB) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceId
|
||||
func (f *mockDB) GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) { return nil, nil }
|
||||
func (f *mockDB) GetAllInterfaces(ctx context.Context) ([]domain.Interface, error) {
|
||||
if f.iface != nil {
|
||||
return []domain.Interface{*f.iface}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (f *mockDB) GetInterfaceIps(ctx context.Context) (map[domain.InterfaceIdentifier][]domain.Cidr, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -192,3 +197,58 @@ func TestCreatePeer_SetsIdentifier_FromPublicKey(t *testing.T) {
|
||||
t.Fatalf("expected peer with identifier %q to be saved in DB", expectedId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDefaultPeer_RespectsInterfaceFlag(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := &config.Config{}
|
||||
cfg.Core.CreateDefaultPeer = true
|
||||
|
||||
bus := &mockBus{}
|
||||
ctrlMgr := &ControllerManager{
|
||||
controllers: map[domain.InterfaceBackend]backendInstance{
|
||||
config.LocalBackendName: {Implementation: &mockController{}},
|
||||
},
|
||||
}
|
||||
|
||||
db := &mockDB{
|
||||
iface: &domain.Interface{
|
||||
Identifier: "wg0",
|
||||
Type: domain.InterfaceTypeServer,
|
||||
CreateDefaultPeer: false, // Flag is disabled!
|
||||
},
|
||||
}
|
||||
|
||||
m := Manager{
|
||||
cfg: cfg,
|
||||
bus: bus,
|
||||
db: db,
|
||||
wg: ctrlMgr,
|
||||
}
|
||||
|
||||
userId := domain.UserIdentifier("user@example.com")
|
||||
ctx := domain.SetUserInfo(context.Background(), &domain.ContextUserInfo{Id: userId, IsAdmin: true})
|
||||
|
||||
// Act
|
||||
err := m.CreateDefaultPeer(ctx, userId)
|
||||
|
||||
// Assert
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDefaultPeer returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(db.savedPeers) != 0 {
|
||||
t.Fatalf("expected no peers to be created because interface flag is false, but got %d", len(db.savedPeers))
|
||||
}
|
||||
|
||||
// Now enable the flag and try again
|
||||
db.iface.CreateDefaultPeer = true
|
||||
err = m.CreateDefaultPeer(ctx, userId)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("CreateDefaultPeer returned error after enabling flag: %v", err)
|
||||
}
|
||||
|
||||
if len(db.savedPeers) != 1 {
|
||||
t.Fatalf("expected 1 peer to be created because interface flag is true, but got %d", len(db.savedPeers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +53,13 @@ type Interface struct {
|
||||
SaveConfig bool // automatically persist config changes to the wgX.conf file
|
||||
|
||||
// WG Portal specific
|
||||
DisplayName string // a nice display name/ description for the interface
|
||||
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
|
||||
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
|
||||
DriverType string // the interface driver type (linux, software, ...)
|
||||
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string // the reason why the interface has been disabled
|
||||
DisplayName string // a nice display name/ description for the interface
|
||||
Type InterfaceType // the interface type, either InterfaceTypeServer or InterfaceTypeClient
|
||||
CreateDefaultPeer bool // if true, default peers will be created for this interface
|
||||
Backend InterfaceBackend // the backend that is used to manage the interface (wgctrl, mikrotik, ...)
|
||||
DriverType string // the interface driver type (linux, software, ...)
|
||||
Disabled *time.Time `gorm:"index"` // flag that specifies if the interface is enabled (up) or not (down)
|
||||
DisabledReason string // the reason why the interface has been disabled
|
||||
|
||||
// Default settings for the peer, used for new peers, those settings will be published to ConfigOption options of
|
||||
// the peer config
|
||||
|
||||
Reference in New Issue
Block a user