From dcd6bd0a9488148862dbb45f0d72ac48f56370c7 Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Sun, 1 Mar 2026 01:06:20 +0100 Subject: [PATCH] feat: add systemd sdnotify support (ready, watchdog, stopping) - Add github.com/coreos/go-systemd/v22/daemon dependency - Signal SdNotifyReady after configuration is loaded - Start watchdog goroutine that pings systemd every WatchdogSec/2 - Signal SdNotifyStopping during graceful shutdown - Update systemd unit file: - Type=notify (instead of simple) - WatchdogSec=30 (auto-restart if service hangs) - NotifyAccess=main (only main process can notify) Benefits: - systemd knows when service is truly ready - Automatic detection of hung/frozen service - Better integration with systemd supervision - More accurate service status reporting Co-authored-by: Qwen-Coder --- cmd/ja4sentinel/main.go | 45 +++++++++++++++++++++++++++ go.mod | 3 +- go.sum | 5 +++ packaging/systemd/ja4sentinel.service | 4 ++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/cmd/ja4sentinel/main.go b/cmd/ja4sentinel/main.go index 7bed7e0..2ce3d4d 100644 --- a/cmd/ja4sentinel/main.go +++ b/cmd/ja4sentinel/main.go @@ -10,6 +10,7 @@ import ( "syscall" "time" + "github.com/coreos/go-systemd/v22/daemon" "ja4sentinel/api" "ja4sentinel/internal/capture" "ja4sentinel/internal/config" @@ -66,6 +67,42 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + // Signal readiness to systemd + if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { + appLogger.Warn("main", "Failed to send READY notification to systemd", map[string]string{ + "error": err.Error(), + }) + } + + // Start watchdog goroutine if enabled + watchdogInterval, err := daemon.SdWatchdogEnabled(false) + if err != nil { + appLogger.Warn("main", "Failed to check watchdog status", map[string]string{ + "error": err.Error(), + }) + } + if watchdogInterval > 0 { + appLogger.Info("main", "systemd watchdog enabled", map[string]string{ + "interval": watchdogInterval.String(), + }) + go func() { + ticker := time.NewTicker(watchdogInterval / 2) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog); err != nil { + appLogger.Warn("main", "Failed to send WATCHDOG notification", map[string]string{ + "error": err.Error(), + }) + } + case <-ctx.Done(): + return + } + } + }() + } + // Setup signal handling sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) @@ -176,6 +213,14 @@ func main() { // Graceful shutdown appLogger.Info("main", "Shutting down...", nil) + + // Signal stopping to systemd + if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil { + appLogger.Warn("main", "Failed to send STOPPING notification to systemd", map[string]string{ + "error": err.Error(), + }) + } + cancel() // Close components diff --git a/go.mod b/go.mod index 5c6c212..474f78e 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( ) require ( - golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect + golang.org/x/sys v0.1.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index f4b1070..3f0c33d 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/psanford/tlsfingerprint v0.0.0-20251111180026-c742e470de9b h1:fsP7F1zLHZ4ozxhesg4j8qSsaJxK7Ev9fA2cUtbThec= @@ -13,10 +15,13 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packaging/systemd/ja4sentinel.service b/packaging/systemd/ja4sentinel.service index 1c24447..92d8732 100644 --- a/packaging/systemd/ja4sentinel.service +++ b/packaging/systemd/ja4sentinel.service @@ -5,13 +5,15 @@ After=network.target Wants=network-online.target [Service] -Type=simple +Type=notify User=root Group=root WorkingDirectory=/var/lib/ja4sentinel ExecStart=/usr/bin/ja4sentinel --config /etc/ja4sentinel/config.yml Restart=on-failure RestartSec=5 +WatchdogSec=30 +NotifyAccess=main Environment=JA4SENTINEL_LOG_LEVEL=info # Security hardening (compatible with root for packet capture)