From 03327339827723d1176bc7be397ffe96df513cf5 Mon Sep 17 00:00:00 2001 From: paspo Date: Fri, 10 Apr 2026 23:54:45 +0200 Subject: [PATCH] Added healthcheck --- Dockerfile | 2 ++ health/health.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 11 +++++++++ 3 files changed, 72 insertions(+) create mode 100644 health/health.go diff --git a/Dockerfile b/Dockerfile index 2bba7bb..7ff3af7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,8 @@ LABEL org.opencontainers.image.licenses=GPLv3 COPY --from=build /endlessh/endlessh /endlessh EXPOSE 2222 2112 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD ["/endlessh", "-healthcheck"] USER nobody ENTRYPOINT ["/endlessh"] CMD ["-logtostderr", "-v=1"] diff --git a/health/health.go b/health/health.go new file mode 100644 index 0000000..f29bf95 --- /dev/null +++ b/health/health.go @@ -0,0 +1,59 @@ +// Copyright (C) 2026 Paolo Asperti +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +package health + +import ( + "net" + "os" + "time" + + "github.com/golang/glog" +) + +const ( + DefaultPort = "51000" + DefaultTimeout = 3 * time.Second +) + +func StartListener(port string) { + go func() { + l, err := net.Listen("tcp", "127.0.0.1:"+port) + if err != nil { + glog.Errorf("Error listening for healthcheck on 127.0.0.1:%v: %v", port, err) + os.Exit(1) + } + defer l.Close() + for { + conn, err := l.Accept() + if err != nil { + glog.Errorf("Error accepting healthcheck connection: %v", err) + os.Exit(1) + } + conn.Close() + } + }() +} + +func Probe(port string) bool { + timeout := DefaultTimeout + conn, err := net.DialTimeout("tcp", "127.0.0.1:"+port, timeout) + if err != nil { + return false + } + conn.Close() + return true +} diff --git a/main.go b/main.go index 2407950..d09b8f9 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ package main import ( "endlessh-go/client" "endlessh-go/geoip" + "endlessh-go/health" "endlessh-go/metrics" "flag" "fmt" @@ -145,6 +146,8 @@ func main() { maxMindDbFileName := flag.String("max_mind_db", "", "Path to the MaxMind DB file.") proxyProtocolEnabled := flag.Bool("proxy_protocol_enabled", false, "Enable PROXY protocol support. This causes the server to expect PROXY protocol headers on incoming connections.") proxyProtocolReadHeaderTimeout := flag.Int("proxy_protocol_read_header_timeout_ms", 200, "Timeout for reading the PROXY protocol header in milliseconds. If the connection does not send a valid PROXY protocol header in this time, the header is ignored.") + healthcheckPort := flag.String("healthcheck_port", health.DefaultPort, "TCP port for container healthcheck; accepts connectionsand closes without logging or metrics") + healthcheck := flag.Bool("healthcheck", false, "Dial healthcheck_port on 127.0.0.1 and exit 0 if reachable (for container healthcheck)") flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage of %v \n", os.Args[0]) @@ -152,6 +155,13 @@ func main() { } flag.Parse() + if *healthcheck { + if !health.Probe(*healthcheckPort) { + os.Exit(1) + } + os.Exit(0) + } + if *prometheusEnabled { if *connType == "tcp6" && *prometheusHost == "0.0.0.0" { *prometheusHost = "[::]" @@ -183,6 +193,7 @@ func main() { if len(connPorts) == 0 { connPorts = append(connPorts, defaultPort) } + health.StartListener(*healthcheckPort) for _, connPort := range connPorts { startAccepting(*maxClients, *connType, *connHost, connPort, interval, clients, records, *proxyProtocolEnabled, *proxyProtocolReadHeaderTimeout) }