server: multi-address listeners, env-file config, self-update timer + checksums
- Comma-separated ECHOLOT_{CONTROL,UDP,TCP}_LISTEN; one listener/socket per
address. Explicit binds matter on multi-IP hosts (a wildcard would also
claim the SSH-only management address) and per-address UDP sockets are
the substrate stun-5780 needs.
- systemd unit reads /etc/echolot-server.env (seeded once, never
overwritten); --install-systemd with --self-update-api also installs a
daily randomized update timer that try-restarts the service.
- selfupdate: SHA256SUMS verification is now mandatory before the atomic
replace (integrity, not authenticity — signing still TODO).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4de3064f71
commit
43e1ba778a
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Package system implements native-host lifecycle: systemd unit install /
|
||||
// uninstall. Linux-only by nature; on other OSes the commands fail with a
|
||||
// clear message rather than pretending.
|
||||
// uninstall, plus an optional self-update timer. Linux-only by nature; on
|
||||
// other OSes the commands fail with a clear message rather than pretending.
|
||||
package system
|
||||
|
||||
import (
|
||||
@@ -14,7 +14,12 @@ import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
const unitPath = "/etc/systemd/system/echolot-server.service"
|
||||
const (
|
||||
unitPath = "/etc/systemd/system/echolot-server.service"
|
||||
updateUnitPath = "/etc/systemd/system/echolot-server-update.service"
|
||||
updateTimerPath = "/etc/systemd/system/echolot-server-update.timer"
|
||||
envFilePath = "/etc/echolot-server.env"
|
||||
)
|
||||
|
||||
const unitTemplate = `[Unit]
|
||||
Description=Echolot probe server
|
||||
@@ -29,6 +34,8 @@ Restart=on-failure
|
||||
RestartSec=5
|
||||
StateDirectory=echolot-server
|
||||
Environment=ECHOLOT_STATE_DIR=/var/lib/echolot-server
|
||||
# Host-specific config (listen addresses etc.) lives here, not in the unit:
|
||||
EnvironmentFile=-%s
|
||||
# Hardening — the server needs sockets and its state dir, nothing else.
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
@@ -40,9 +47,43 @@ PrivateTmp=true
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
// InstallSystemd writes the unit for THIS binary (absolute path), reloads
|
||||
// systemd, and enables the service. Idempotent.
|
||||
func InstallSystemd(extraArgs []string) error {
|
||||
const updateUnitTemplate = `[Unit]
|
||||
Description=Echolot server self-update
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=%s --self-update --self-update-api=%s
|
||||
# The updater only replaces the binary; the restart activates it.
|
||||
ExecStartPost=/usr/bin/systemctl try-restart echolot-server.service
|
||||
`
|
||||
|
||||
const updateTimerTemplate = `[Unit]
|
||||
Description=Daily Echolot server self-update check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=daily
|
||||
RandomizedDelaySec=1h
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
`
|
||||
|
||||
const envFileTemplate = `# Echolot server host configuration (systemd EnvironmentFile).
|
||||
# Bind explicit addresses on multi-IP hosts — a wildcard would also claim
|
||||
# management-only addresses. Comma-separated lists are supported.
|
||||
#ECHOLOT_CONTROL_LISTEN=203.0.113.10:8443,[2001:db8::10]:8443
|
||||
#ECHOLOT_UDP_LISTEN=203.0.113.10:8442,[2001:db8::10]:8442
|
||||
#ECHOLOT_TCP_LISTEN=203.0.113.10:8441,[2001:db8::10]:8441
|
||||
#ECHOLOT_ADMIN_LISTEN=127.0.0.1:8444
|
||||
#ECHOLOT_NAME=my-server
|
||||
`
|
||||
|
||||
// InstallSystemd writes the unit(s) for THIS binary (absolute path), reloads
|
||||
// systemd, and enables the service. When selfUpdateAPI is non-empty, a daily
|
||||
// self-update timer is installed alongside. Idempotent.
|
||||
func InstallSystemd(selfUpdateAPI string) error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return fmt.Errorf("--install-systemd is Linux-only (this is %s)", runtime.GOOS)
|
||||
}
|
||||
@@ -54,22 +95,36 @@ func InstallSystemd(extraArgs []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
execStart := self
|
||||
for _, a := range extraArgs {
|
||||
execStart += " " + a
|
||||
}
|
||||
if err := os.WriteFile(unitPath, []byte(fmt.Sprintf(unitTemplate, execStart)), 0o644); err != nil {
|
||||
if err := os.WriteFile(unitPath, []byte(fmt.Sprintf(unitTemplate, self, envFilePath)), 0o644); err != nil {
|
||||
return fmt.Errorf("writing %s (need root?): %w", unitPath, err)
|
||||
}
|
||||
for _, cmd := range [][]string{
|
||||
// Seed the env file once; never overwrite an existing one.
|
||||
if _, err := os.Stat(envFilePath); os.IsNotExist(err) {
|
||||
_ = os.WriteFile(envFilePath, []byte(envFileTemplate), 0o644)
|
||||
}
|
||||
cmds := [][]string{
|
||||
{"systemctl", "daemon-reload"},
|
||||
{"systemctl", "enable", "--now", "echolot-server.service"},
|
||||
} {
|
||||
}
|
||||
if selfUpdateAPI != "" {
|
||||
if err := os.WriteFile(updateUnitPath,
|
||||
[]byte(fmt.Sprintf(updateUnitTemplate, self, selfUpdateAPI)), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(updateTimerPath, []byte(updateTimerTemplate), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
cmds = append(cmds, []string{"systemctl", "enable", "--now", "echolot-server-update.timer"})
|
||||
}
|
||||
for _, cmd := range cmds {
|
||||
if out, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%v: %s: %w", cmd, out, err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("installed + started echolot-server.service (ExecStart=%s)\n", execStart)
|
||||
fmt.Printf("installed echolot-server.service (ExecStart=%s, config: %s)\n", self, envFilePath)
|
||||
if selfUpdateAPI != "" {
|
||||
fmt.Println("installed echolot-server-update.timer (daily, randomized)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,11 +133,14 @@ func UninstallSystemd() error {
|
||||
return fmt.Errorf("--uninstall-systemd is Linux-only (this is %s)", runtime.GOOS)
|
||||
}
|
||||
// Stop/disable first; ignore "not loaded" errors so uninstall is idempotent.
|
||||
_ = exec.Command("systemctl", "disable", "--now", "echolot-server-update.timer").Run()
|
||||
_ = exec.Command("systemctl", "disable", "--now", "echolot-server.service").Run()
|
||||
if err := os.Remove(unitPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
for _, p := range []string{unitPath, updateUnitPath, updateTimerPath} {
|
||||
if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_ = exec.Command("systemctl", "daemon-reload").Run()
|
||||
fmt.Println("removed echolot-server.service (state dir left in place)")
|
||||
fmt.Println("removed echolot-server units (state dir and env file left in place)")
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user