Files
cunicu/pkg/daemon/daemon.go
Steffen Vogel 3bee839348 fix: Update copyright years
Signed-off-by: Steffen Vogel <post@steffenvogel.de>
2025-01-01 22:45:39 +01:00

338 lines
7.2 KiB
Go

// SPDX-FileCopyrightText: 2023-2025 Steffen Vogel <post@steffenvogel.de>
// SPDX-License-Identifier: Apache-2.0
package daemon
import (
"errors"
"fmt"
"strings"
"time"
"go.uber.org/zap"
"golang.zx2c4.com/wireguard/wgctrl"
"cunicu.li/cunicu/pkg/config"
"cunicu.li/cunicu/pkg/device"
"cunicu.li/cunicu/pkg/log"
osx "cunicu.li/cunicu/pkg/os"
"cunicu.li/cunicu/pkg/os/systemd"
"cunicu.li/cunicu/pkg/signaling"
"cunicu.li/cunicu/pkg/wg"
)
var (
errInsufficientPrivileges = errors.New("insufficient privileges. Please run cunicu with administrator privileges")
ErrFeatureDeactivated = errors.New("feature deactivated")
)
type State string
const (
StateStarted = "started"
StateInitializing = "initializing"
StateReady = "ready"
StateReloading = "reloading"
StateStopping = "stoppping"
StateSynchronizing = "syncing"
)
type Daemon struct {
*Watcher
Backend *signaling.MultiBackend
Client *wgctrl.Client
Config *config.Config
devices []device.Device
state State
stop chan any
reexecOnClose bool
logger *log.Logger
}
func NewDaemon(cfg *config.Config) (*Daemon, error) {
var err error
// Check permissions
if !osx.HasAdminPrivileges() {
return nil, errInsufficientPrivileges
}
d := &Daemon{
Config: cfg,
devices: []device.Device{},
stop: make(chan any),
state: StateStarted,
logger: log.Global.Named("daemon"),
}
// Create WireGuard netlink socket
if d.Client, err = wgctrl.New(); err != nil {
return nil, fmt.Errorf("failed to create WireGuard client: %w", err)
}
// Create watcher
if d.Watcher, err = NewWatcher(d.Client, cfg.WatchInterval, cfg.InterfaceFilter); err != nil {
return nil, fmt.Errorf("failed to initialize watcher: %w", err)
}
// Create signaling backend
if d.Backend, err = signaling.NewMultiBackend(cfg.Backends, &signaling.BackendConfig{}); err != nil {
return nil, fmt.Errorf("failed to initialize signaling backend: %w", err)
}
d.AddInterfaceHandler(d)
return d, nil
}
// Start starts the daemon and blocks until Shutdown() is called.
func (d *Daemon) Start() error {
if err := d.setState(StateInitializing); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
if err := wg.CleanupUserSockets(); err != nil {
return fmt.Errorf("failed to cleanup stale user space sockets: %w", err)
}
if err := d.CreateDevices(); err != nil {
return fmt.Errorf("failed to create devices: %w", err)
}
go d.Watcher.Watch()
if err := d.Watcher.Sync(); err != nil {
return fmt.Errorf("initial sync failed: %w", err)
}
signals := osx.SetupSignals(osx.SigUpdate, osx.SigReload)
wdt, err := d.watchdogTicker()
if err != nil && !errors.Is(err, errNotSupported) {
return fmt.Errorf("failed to get watchdog interval: %w", err)
}
if err := d.setState(StateReady); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
out:
for {
select {
case sig := <-signals:
d.logger.Debug("Received signal", zap.Any("signal", sig))
switch sig {
case osx.SigUpdate:
if err := d.Sync(); err != nil {
d.logger.Error("Failed to synchronize interfaces", zap.Error(err))
}
case osx.SigReload:
if err := d.reload(); err != nil {
return err
}
default:
break out
}
case <-wdt:
if _, err := systemd.Notify(false, systemd.NotifyWatchdog); err != nil {
return fmt.Errorf("failed to notify systemd watchdog: %w", err)
}
d.logger.DebugV(20, "Watchdog tick")
case <-d.stop:
break out
}
}
return nil
}
// Shutdown stops the daemon.
func (d *Daemon) Shutdown(restart bool) {
if d.stop == nil {
return
}
close(d.stop)
d.stop = nil
if restart {
d.reexecOnClose = true
d.logger.Debug("Restarting daemon")
} else {
d.logger.Debug("Stopping daemon")
}
}
func (d *Daemon) Sync() error {
if err := d.Watcher.Sync(); err != nil {
return err
}
for _, i := range d.interfaces {
if err := i.SyncFeatures(); err != nil {
return err
}
}
return nil
}
func (d *Daemon) Close() error {
if err := d.setState(StateStopping); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
if err := d.Watcher.Close(); err != nil {
return fmt.Errorf("failed to close watcher: %w", err)
}
for _, i := range d.interfaces {
if err := i.Close(); err != nil {
return fmt.Errorf("failed to close interface: %w", err)
}
}
if err := d.Backend.Close(); err != nil {
return fmt.Errorf("failed to close signaling backend: %w", err)
}
for _, dev := range d.devices {
if err := dev.Close(); err != nil {
return fmt.Errorf("failed to delete device: %w", err)
}
}
if err := d.Client.Close(); err != nil {
return fmt.Errorf("failed to close WireGuard client: %w", err)
}
if d.reexecOnClose {
return osx.ReexecSelf()
}
d.logger.Debug("Closed daemon")
return nil
}
func (d *Daemon) CreateDevices() error {
devs, err := d.Client.Devices()
if err != nil {
return fmt.Errorf("failed to get existing WireGuard devices: %w", err)
}
isPattern := func(s string) bool {
return strings.ContainsAny(s, "*?[]\\")
}
alreadyExists := func(s string) bool {
for _, dev := range devs {
if dev.Name == s {
return true
}
}
for _, dev := range d.devices {
if dev.Name() == s {
return true
}
}
return false
}
for _, name := range d.Config.InterfaceOrder {
if isPattern(name) {
continue // Patterns are ignored
}
if alreadyExists(name) {
continue // Device already exists
}
icfg := d.Config.InterfaceSettings(name)
dev, err := device.NewDevice(name, icfg.UserSpace)
if err != nil {
return fmt.Errorf("failed to create WireGuard device: %w", err)
}
d.devices = append(d.devices, dev)
}
return nil
}
func (d *Daemon) watchdogTicker() (<-chan time.Time, error) {
wdInterval, err := systemd.WatchdogEnabled(true)
if err != nil {
return nil, err
} else if wdInterval == 0 {
d.logger.DebugV(5, "Not started via systemd. Disabling watchdog")
return nil, errNotSupported
}
return time.NewTicker(wdInterval / 2).C, nil
}
func (d *Daemon) reload() error {
if err := d.setState(StateReloading); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
if _, err := d.Config.ReloadAllSources(); err != nil {
d.logger.Error("Failed to reload config", zap.Error(err))
}
if err := d.setState(StateReady); err != nil {
return fmt.Errorf("failed transition state: %w", err)
}
return nil
}
func (d *Daemon) setState(s State) error {
d.state = s
d.logger.DebugV(5, "Daemon state changed", zap.String("state", string(s)))
switch d.state {
case StateStarted:
case StateInitializing:
case StateSynchronizing:
case StateReady:
if _, err := systemd.Notify(false, systemd.NotifyReady); err != nil {
return fmt.Errorf("failed to notify systemd: %w", err)
}
case StateReloading:
now, err := osx.GetClockMonotonic()
if err != nil {
return fmt.Errorf("failed to get monotonic time: %w", err)
}
msgMonotonicTime := fmt.Sprintf("MONOTONIC_USEC=%d", now.UnixMicro())
if _, err := systemd.Notify(false, systemd.NotifyReloading, msgMonotonicTime); err != nil {
return fmt.Errorf("failed to notify systemd: %w", err)
}
case StateStopping:
if _, err := systemd.Notify(false, systemd.NotifyStopping); err != nil {
return fmt.Errorf("failed to notify systemd: %w", err)
}
}
return nil
}