// This file is part of the happyDomain (R) project. // Copyright (c) 2020-2026 happyDomain // Authors: Pierre-Olivier Mercier, et al. // // This program is offered under a commercial and under the AGPL license. // For commercial licensing, contact us at . // // For AGPL licensing: // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero 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 Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package checker import ( "context" "fmt" "strconv" sdk "git.happydns.org/checker-sdk-go/checker" ) // reachabilityRule reports per-endpoint TCP reachability. One state // per probed (ip, port) pair so the UI distinguishes individual // firewall / routing issues. type reachabilityRule struct{} func (r *reachabilityRule) Name() string { return "ssh.tcp_reachable" } func (r *reachabilityRule) Description() string { return "Verifies that every probed (address, port) pair accepts a TCP connection." } func (r *reachabilityRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadSSHData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } if len(data.Endpoints) == 0 { return []sdk.CheckState{noEndpointsState("ssh.tcp_reachable.no_endpoints")} } var states []sdk.CheckState for _, ep := range data.Endpoints { if ep.TCPConnected { continue } msg := "Cannot open TCP connection to " + ep.Address if ep.Error != "" { msg += ": " + ep.Error } states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Message: msg, Code: "tcp_unreachable", Subject: ep.Address, Meta: map[string]any{ "fix": "Check DNS, firewall (allow tcp/" + strconv.Itoa(int(ep.Port)) + " from the internet), and that sshd is running.", }, }) } if len(states) == 0 { return []sdk.CheckState{passState("ssh.tcp_reachable.ok", "All probed endpoints accept TCP connections.")} } return states } // handshakeRule reports per-endpoint SSH handshake progress: whether // banner exchange and KEXINIT parsing completed. Endpoints that are // TCP-unreachable are skipped (covered by reachabilityRule). type handshakeRule struct{} func (r *handshakeRule) Name() string { return "ssh.handshake" } func (r *handshakeRule) Description() string { return "Verifies that the SSH banner exchange and KEXINIT parse succeed on every reachable endpoint." } func (r *handshakeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState { data, errSt := loadSSHData(ctx, obs) if errSt != nil { return []sdk.CheckState{*errSt} } if len(data.Endpoints) == 0 { return []sdk.CheckState{noEndpointsState("ssh.handshake.no_endpoints")} } var states []sdk.CheckState for _, ep := range data.Endpoints { if !ep.TCPConnected { continue } switch ep.Stage { case "banner": states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "no_ssh_banner", Subject: ep.Address, Message: fmt.Sprintf("Server on %s did not send an SSH-2.0 banner: %s", ep.Address, ep.Error), Meta: map[string]any{"fix": "Check that an SSH daemon (not HTTP, mail, ...) listens on this port."}, }) case "banner_write": states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "banner_write_failed", Subject: ep.Address, Message: "Failed to send our client banner: " + ep.Error, }) case "kexinit_read": states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "kexinit_read_failed", Subject: ep.Address, Message: "Server did not send KEXINIT after banner: " + ep.Error, }) case "kexinit_parse": states = append(states, sdk.CheckState{ Status: sdk.StatusCrit, Code: "kexinit_parse_failed", Subject: ep.Address, Message: "Malformed KEXINIT packet: " + ep.Error, }) } } if len(states) == 0 { return []sdk.CheckState{passState("ssh.handshake.ok", "All reachable endpoints completed the SSH handshake.")} } return states }