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
@@ -63,8 +63,10 @@ func run() error {
|
||||
fmt.Println(Version)
|
||||
return nil
|
||||
case actions.InstallSystemd:
|
||||
// The unit runs this same binary in serve mode with env-based config.
|
||||
return system.InstallSystemd(nil)
|
||||
// The unit runs this same binary in serve mode; host config comes
|
||||
// from /etc/echolot-server.env. A self-update timer is installed
|
||||
// only when an update API is configured.
|
||||
return system.InstallSystemd(cfg.SelfUpdateAPI)
|
||||
case actions.UninstallSystemd:
|
||||
return system.UninstallSystemd()
|
||||
case actions.SelfUpdate:
|
||||
@@ -95,20 +97,31 @@ func serve(cfg *config.Config) error {
|
||||
sessions := session.NewManager(15 * time.Minute)
|
||||
ctl := &control.Server{
|
||||
Store: st, Sessions: sessions, Name: cfg.Name,
|
||||
UDPPort: mustPort(cfg.UDPListen), TCPPort: mustPort(cfg.TCPListen), PinB64: pin,
|
||||
UDPPort: mustPort(firstAddr(cfg.UDPListen)), TCPPort: mustPort(firstAddr(cfg.TCPListen)), PinB64: pin,
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
errCh := make(chan error, 4)
|
||||
ctlAddrs := config.Addrs(cfg.ControlListen)
|
||||
udpAddrs := config.Addrs(cfg.UDPListen)
|
||||
errCh := make(chan error, len(ctlAddrs)+len(udpAddrs)+2)
|
||||
|
||||
// Control plane (HTTPS, pin-based trust)
|
||||
// Control plane (HTTPS, pin-based trust) — one shared server, one
|
||||
// listener per configured address; Shutdown closes them all.
|
||||
ctlSrv := &http.Server{
|
||||
Addr: cfg.ControlListen, Handler: ctl.Handler(),
|
||||
Handler: ctl.Handler(),
|
||||
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12},
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
go func() { errCh <- fmt.Errorf("control: %w", ctlSrv.ListenAndServeTLS("", "")) }()
|
||||
for _, addr := range ctlAddrs {
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("control listen %s: %w", addr, err)
|
||||
}
|
||||
go func(a string, l net.Listener) {
|
||||
errCh <- fmt.Errorf("control %s: %w", a, ctlSrv.ServeTLS(l, "", ""))
|
||||
}(addr, ln)
|
||||
}
|
||||
|
||||
// Admin/health (plain HTTP, localhost by default; spec §7)
|
||||
admin := http.NewServeMux()
|
||||
@@ -129,20 +142,28 @@ func serve(cfg *config.Config) error {
|
||||
adminSrv := &http.Server{Addr: cfg.AdminListen, Handler: admin, ReadHeaderTimeout: 10 * time.Second}
|
||||
go func() { errCh <- fmt.Errorf("admin: %w", adminSrv.ListenAndServe()) }()
|
||||
|
||||
// UDP data plane
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", cfg.UDPListen)
|
||||
if err != nil {
|
||||
return err
|
||||
// UDP data plane — one socket per configured address. Distinct sockets
|
||||
// (not wildcard) also guarantee responses leave from the address the
|
||||
// request arrived on, which stun-5780 will rely on.
|
||||
var udpConns []*net.UDPConn
|
||||
for _, addr := range udpAddrs {
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("udp addr %s: %w", addr, err)
|
||||
}
|
||||
conn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("udp listen %s: %w", addr, err)
|
||||
}
|
||||
udpConns = append(udpConns, conn)
|
||||
dp := &dataplane.Server{Sessions: sessions}
|
||||
go func(a string, c *net.UDPConn) {
|
||||
errCh <- fmt.Errorf("udp %s: %w", a, dp.Serve(c))
|
||||
}(addr, conn)
|
||||
}
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("udp listen: %w", err)
|
||||
}
|
||||
dp := &dataplane.Server{Sessions: sessions}
|
||||
go func() { errCh <- fmt.Errorf("udp: %w", dp.Serve(udpConn)) }()
|
||||
|
||||
slog.Info("listening",
|
||||
"control", cfg.ControlListen, "admin", cfg.AdminListen, "udp", cfg.UDPListen)
|
||||
"control", ctlAddrs, "admin", cfg.AdminListen, "udp", udpAddrs)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -151,13 +172,22 @@ func serve(cfg *config.Config) error {
|
||||
defer cancel()
|
||||
_ = ctlSrv.Shutdown(shutCtx)
|
||||
_ = adminSrv.Shutdown(shutCtx)
|
||||
_ = udpConn.Close()
|
||||
for _, c := range udpConns {
|
||||
_ = c.Close()
|
||||
}
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func firstAddr(spec string) string {
|
||||
if a := config.Addrs(spec); len(a) > 0 {
|
||||
return a[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mustPort(listen string) int {
|
||||
_, p, err := net.SplitHostPort(listen)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user