mirror of
https://codeberg.org/cunicu/cunicu.git
synced 2025-09-26 21:01:14 +08:00
338 lines
7.2 KiB
Go
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
|
|
}
|