Implements the from-scratch multi-server WireGuard management fork per CLAUDE.md spec: sqlite schema (servers/peers/audit_log/users), Curve25519 key generation, per-interface config rendering + wg-quick/systemd control, nftables hook scaffolding, session+CSRF-protected REST API with QR code and config download endpoints, a minimal vanilla-JS web UI, legacy wg0.conf migration, and both a native installer and a Proxmox LXC provisioning script (with auto-detected latest Debian template). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
206 lines
5.8 KiB
Go
206 lines
5.8 KiB
Go
package server
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
|
|
"gitea.perlbach24.de/scripte/wireguard-ui-multi/internal/database"
|
|
)
|
|
|
|
// Server represents a single, independent WireGuard interface.
|
|
type Server struct {
|
|
ID int64
|
|
Name string
|
|
InterfaceName string
|
|
ListenPort int
|
|
PrivateKey string
|
|
PublicKey string
|
|
AddressRange string
|
|
DNS string
|
|
MTU int
|
|
Enabled bool
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// Peer represents a WireGuard client belonging to a Server.
|
|
type Peer struct {
|
|
ID int64
|
|
ServerID int64
|
|
Name string
|
|
Email string
|
|
PublicKey string
|
|
PrivateKey string
|
|
PresharedKey string
|
|
AllowedIPs string
|
|
Endpoint string
|
|
PersistentKeepalive int
|
|
Enabled bool
|
|
ExpiresAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
var ErrNotFound = errors.New("not found")
|
|
|
|
// Store provides CRUD access to servers and peers.
|
|
type Store struct {
|
|
db *database.DB
|
|
}
|
|
|
|
func NewStore(db *database.DB) *Store {
|
|
return &Store{db: db}
|
|
}
|
|
|
|
func (s *Store) CreateServer(srv *Server) (int64, error) {
|
|
res, err := s.db.Exec(`INSERT INTO servers
|
|
(name, interface_name, listen_port, private_key, public_key, address_range, dns, mtu, enabled)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
srv.Name, srv.InterfaceName, srv.ListenPort, srv.PrivateKey, srv.PublicKey,
|
|
srv.AddressRange, srv.DNS, srv.MTU, boolToInt(srv.Enabled))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (s *Store) UpdateServer(srv *Server) error {
|
|
_, err := s.db.Exec(`UPDATE servers SET
|
|
name = ?, interface_name = ?, listen_port = ?, private_key = ?, public_key = ?,
|
|
address_range = ?, dns = ?, mtu = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?`,
|
|
srv.Name, srv.InterfaceName, srv.ListenPort, srv.PrivateKey, srv.PublicKey,
|
|
srv.AddressRange, srv.DNS, srv.MTU, boolToInt(srv.Enabled), srv.ID)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) DeleteServer(id int64) error {
|
|
_, err := s.db.Exec(`DELETE FROM servers WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetServer(id int64) (*Server, error) {
|
|
row := s.db.QueryRow(`SELECT id, name, interface_name, listen_port, private_key, public_key,
|
|
address_range, dns, mtu, enabled, created_at, updated_at FROM servers WHERE id = ?`, id)
|
|
return scanServer(row)
|
|
}
|
|
|
|
func (s *Store) ListServers() ([]*Server, error) {
|
|
rows, err := s.db.Query(`SELECT id, name, interface_name, listen_port, private_key, public_key,
|
|
address_range, dns, mtu, enabled, created_at, updated_at FROM servers ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []*Server
|
|
for rows.Next() {
|
|
srv, err := scanServerRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, srv)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Store) CreatePeer(p *Peer) (int64, error) {
|
|
res, err := s.db.Exec(`INSERT INTO peers
|
|
(server_id, name, email, public_key, private_key, preshared_key, allowed_ips, endpoint,
|
|
persistent_keepalive, enabled, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
p.ServerID, p.Name, p.Email, p.PublicKey, p.PrivateKey, p.PresharedKey, p.AllowedIPs,
|
|
p.Endpoint, p.PersistentKeepalive, boolToInt(p.Enabled), p.ExpiresAt)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (s *Store) UpdatePeer(p *Peer) error {
|
|
_, err := s.db.Exec(`UPDATE peers SET
|
|
name = ?, email = ?, public_key = ?, preshared_key = ?, allowed_ips = ?, endpoint = ?,
|
|
persistent_keepalive = ?, enabled = ?, expires_at = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?`,
|
|
p.Name, p.Email, p.PublicKey, p.PresharedKey, p.AllowedIPs, p.Endpoint,
|
|
p.PersistentKeepalive, boolToInt(p.Enabled), p.ExpiresAt, p.ID)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) DeletePeer(id int64) error {
|
|
_, err := s.db.Exec(`DELETE FROM peers WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) GetPeer(id int64) (*Peer, error) {
|
|
row := s.db.QueryRow(`SELECT id, server_id, name, email, public_key, private_key, preshared_key,
|
|
allowed_ips, endpoint, persistent_keepalive, enabled, expires_at, created_at, updated_at
|
|
FROM peers WHERE id = ?`, id)
|
|
return scanPeer(row)
|
|
}
|
|
|
|
func (s *Store) ListPeersByServer(serverID int64) ([]*Peer, error) {
|
|
rows, err := s.db.Query(`SELECT id, server_id, name, email, public_key, private_key, preshared_key,
|
|
allowed_ips, endpoint, persistent_keepalive, enabled, expires_at, created_at, updated_at
|
|
FROM peers WHERE server_id = ? ORDER BY name`, serverID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []*Peer
|
|
for rows.Next() {
|
|
p, err := scanPeerRows(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
type scanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanServer(row scanner) (*Server, error) {
|
|
var srv Server
|
|
var enabled int
|
|
if err := row.Scan(&srv.ID, &srv.Name, &srv.InterfaceName, &srv.ListenPort, &srv.PrivateKey,
|
|
&srv.PublicKey, &srv.AddressRange, &srv.DNS, &srv.MTU, &enabled, &srv.CreatedAt, &srv.UpdatedAt); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
srv.Enabled = enabled != 0
|
|
return &srv, nil
|
|
}
|
|
|
|
func scanServerRows(rows *sql.Rows) (*Server, error) { return scanServer(rows) }
|
|
|
|
func scanPeer(row scanner) (*Peer, error) {
|
|
var p Peer
|
|
var enabled int
|
|
if err := row.Scan(&p.ID, &p.ServerID, &p.Name, &p.Email, &p.PublicKey, &p.PrivateKey,
|
|
&p.PresharedKey, &p.AllowedIPs, &p.Endpoint, &p.PersistentKeepalive, &enabled,
|
|
&p.ExpiresAt, &p.CreatedAt, &p.UpdatedAt); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
p.Enabled = enabled != 0
|
|
return &p, nil
|
|
}
|
|
|
|
func scanPeerRows(rows *sql.Rows) (*Peer, error) { return scanPeer(rows) }
|
|
|
|
func boolToInt(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|