diff --git a/Makefile b/Makefile index 89c8fef2..761852c7 100644 --- a/Makefile +++ b/Makefile @@ -740,6 +740,7 @@ DOC_PLUGINS=\ check_http \ check_nsc_web \ check_tcp \ + check_ssh \ # skip markdown help files for these commands DOC_EXCLUDES=\ diff --git a/docs/checks/plugins/check_ssh.md b/docs/checks/plugins/check_ssh.md new file mode 100644 index 00000000..d1649866 --- /dev/null +++ b/docs/checks/plugins/check_ssh.md @@ -0,0 +1,85 @@ +--- +title: check_ssh +--- + +## check_ssh + +Runs check_tcp with an SSH configururation to check for a running SSH server. +It basically wraps the plugin from https://github.com/taku-k/go-check-plugins/tree/master/check-tcp + +- [Examples](#examples) +- [Usage](#usage) + +## Implementation + +| Windows | Linux | FreeBSD | MacOSX | +|:------------------:|:------------------:|:------------------:|:------------------:| +| :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | + +## Examples + +### Default Check + + check_ssh github.com +SSH OK - 0.234 seconds response time on github.com port 22 [SSH-2.0-8ad108e] | time=0.234029s;;;0.000000;10.000000 + + check_ssh --hostname github.com --warning 1 +SSH OK - 0.262 seconds response time on github.com port 22 [SSH-2.0-8ad108e] | time=0.262048s;;;1.000000;10.000000 + +### Example using NRPE and Naemon + +Naemon Config + + define command{ + command_name check_nrpe + command_line $USER1$/check_nrpe -H $HOSTADDRESS$ -n -c $ARG1$ -a $ARG2$ + } + + define service { + host_name testhost + service_description check_ssh + use generic-service + check_command check_nrpe!check_ssh!'-H' '192.168.178.100' '-p' '2323' + } + +## Usage + +```Usage: + check_tcp [OPTIONS] + +Application Options: + --service= Service name. e.g. ftp, smtp, pop, imap and so on + -H, --hostname= Host name or IP Address + -p, --port= Port number + -s, --send= String to send to the server + -e, --expect-pattern= Regexp pattern to expect in server response + -q, --quit= String to send server to initiate a clean close + of the connection + -S, --ssl Use SSL for the connection. + -U, --unix-sock= Unix Domain Socket + --no-check-certificate Do not check certificate + -t, --timeout= Seconds before connection times out (default: 10) + -m, --maxbytes= Close connection once more than this number of + bytes are received + -d, --delay= Seconds to wait between sending string and + polling for response + -w, --warning= Response time to result in warning status + (seconds) + -c, --critical= Response time to result in critical status + (seconds) (default: 10) + -E, --escape Can use \n, \r, \t or \ in send or quit string. + Must come before send or quit option. By default, + nothing added to send, \r\n added to end of quit + -W, --error-warning Set the error level to warning when exiting with + unexpected error (default: critical). In the case + of request succeeded, evaluation result of -c + option eval takes priority. + -C, --expect-closed Verify that the port/unixsock is closed. If the + port/unixsock is closed, OK; if open, follow the + ErrWarning flag. This option only verifies the + connection. + -v, --verbose Enables verbose logging of the actions taken. + +Help Options: + -h, --help Show this help message +``` diff --git a/docs/checks/plugins/check_tcp.md b/docs/checks/plugins/check_tcp.md index 156868d1..b398a308 100644 --- a/docs/checks/plugins/check_tcp.md +++ b/docs/checks/plugins/check_tcp.md @@ -87,6 +87,7 @@ Application Options: port/unixsock is closed, OK; if open, follow the ErrWarning flag. This option only verifies the connection. + -v, --verbose Enables verbose logging of the actions taken. Help Options: -h, --help Show this help message diff --git a/pkg/check_tcp/check_ssh.go b/pkg/check_tcp/check_ssh.go new file mode 100644 index 00000000..1880adda --- /dev/null +++ b/pkg/check_tcp/check_ssh.go @@ -0,0 +1,55 @@ +package check_tcp + +import ( + "context" + "fmt" + "io" + "strings" +) + +func CheckSSH(_ context.Context, output io.Writer, args []string, sendString string) int { + opts, err := parseArgs(args) + if err != nil { + fmt.Fprintf(output, "%s", err.Error()) + + return 2 + } + + if opts.Port == 0 { + opts.Port = 22 + } + + if opts.Timeout > opts.Critical && opts.Critical != 0 && opts.Verbose { + fmt.Fprintf(output, "Timeout to establish connection: %f is higher than critical threshold: %f\n", opts.Timeout, opts.Critical) + } + + if opts.Timeout > opts.Warning && opts.Warning != 0 && opts.Verbose { + fmt.Fprintf(output, "Timeout to establish connection: %f is higher than warning threshold: %f\n", opts.Timeout, opts.Warning) + } + + opts.Send = sendString + // SSH Tcp connections print out a string: + // nc -v github.com 22 + // Connection to github.com (140.82.121.4) 22 port [tcp/ssh] succeeded! + // SSH-2.0-8ad108e + + // RFC 4253:4.2 + // https://datatracker.ietf.org/doc/html/rfc4253 , Page 5 + // The third field consists of any printable ASCII characters + // EXCEPT whitespaces and minus sign + // The group at the end consists of two parts + // !- includes ASCII characters incl. '!' until '-' but not '-' + // .~ includes ASCII characters incl. '.' until '-' but not '~' + // this skips over the minus sign + opts.ExpectPattern = `^SSH-\d+\.\d+-[!-,.-~]+` + + ckr := opts.run(output) + ckr.Name = "SSH" + if opts.Service != "" { + ckr.Name = opts.Service + } + + fmt.Fprintf(output, "%s %s - %s", ckr.Name, ckr.Status, strings.TrimSpace(ckr.Message)) + + return int(ckr.Status) +} diff --git a/pkg/check_tcp/check_tcp.go b/pkg/check_tcp/check_tcp.go index 804de72c..4df8397b 100644 --- a/pkg/check_tcp/check_tcp.go +++ b/pkg/check_tcp/check_tcp.go @@ -9,6 +9,7 @@ import ( "net" "os" "regexp" + "slices" "strings" "time" @@ -17,13 +18,29 @@ import ( ) func Check(ctx context.Context, output io.Writer, args []string) int { + // snclient supports short arguments with multiple chars like -v and not -vv or -vvv + // if snclient agent has these verbose arguments, they are passed as is to internal checks + // if that is the case, delete them and add -v instead. + hadVerbose := false + args = slices.DeleteFunc(args, func(s string) bool { + isVerbose := s == "-v" || s == "-vv" || s == "-vvv" + if isVerbose { + hadVerbose = true + } + + return isVerbose + }) + if hadVerbose { + args = append(args, "-v") + } + opts, err := parseArgs(args) if err != nil { fmt.Fprintf(output, "%s", err.Error()) return 2 } - ckr := opts.run() + ckr := opts.run(output) ckr.Name = "TCP" if opts.Service != "" { ckr.Name = opts.Service @@ -48,6 +65,7 @@ type tcpOpts struct { Escape bool `short:"E" long:"escape" description:"Can use \\n, \\r, \\t or \\ in send or quit string. Must come before send or quit option. By default, nothing added to send, \\r\\n added to end of quit"` ErrWarning bool `short:"W" long:"error-warning" description:"Set the error level to warning when exiting with unexpected error (default: critical). In the case of request succeeded, evaluation result of -c option eval takes priority."` ExpectClosed bool `short:"C" long:"expect-closed" description:"Verify that the port/unixsock is closed. If the port/unixsock is closed, OK; if open, follow the ErrWarning flag. This option only verifies the connection."` + Verbose bool `short:"v" long:"verbose" description:"Enables verbose logging of the actions taken."` } type exchange struct { @@ -120,6 +138,11 @@ var defaultExchangeMap = map[string]exchange{ Send: "version\n", ExpectPattern: `\A[0-9]+\.[0-9]+\n\z`, }, + "SSH": { + Port: 22, + Send: "SSH-1.0-check_tcp", + ExpectPattern: `^SSH-\d+\.\d+-[!-,.-~]+`, + }, } func (opts *tcpOpts) prepare() error { @@ -174,7 +197,7 @@ func dial(network, address string, ssl bool, noCheckCertificate bool, timeout ti return d.Dial(network, address) } -func (opts *tcpOpts) run() *checkers.Checker { +func (opts *tcpOpts) run(output io.Writer) *checkers.Checker { err := opts.prepare() if err != nil { return checkers.Unknown(err.Error()) @@ -202,6 +225,9 @@ func (opts *tcpOpts) run() *checkers.Checker { time.Sleep(time.Duration(opts.Delay) * time.Second) } + if opts.Verbose { + fmt.Fprintf(output, "Establishing a connection to addr: %s protocol: %s ssl: %t noCheckCertificate: %t timeout: %f\n", addr, proto, opts.SSL, opts.NoCheckCertificate, timeout.Seconds()) + } conn, err := dial(proto, addr, opts.SSL, opts.NoCheckCertificate, timeout) if err != nil { if opts.ExpectClosed { @@ -234,6 +260,9 @@ func (opts *tcpOpts) run() *checkers.Checker { } if opts.Send != "" { + if opts.Verbose { + fmt.Fprintf(output, "Writing to the socket: %s\n", opts.Send) + } err := write(conn, []byte(opts.Send), timeout) if err != nil { if opts.ErrWarning { @@ -258,10 +287,17 @@ func (opts *tcpOpts) run() *checkers.Checker { return checkers.Warning("Unexpected response from host/socket: " + res) } return checkers.Critical("Unexpected response from host/socket: " + res) + } else { + if opts.Verbose { + fmt.Fprintf(output, "Result matched expected regex: '%s'\n", opts.ExpectPattern) + } } } if opts.Quit != "" { + if opts.Verbose { + fmt.Fprintf(output, "Writing to the socket for quitting: %s\n", opts.Quit) + } err := write(conn, []byte(opts.Quit), timeout) if err != nil { if opts.ErrWarning { diff --git a/pkg/snclient/builtin_check.go b/pkg/snclient/builtin_check.go index 17dfe198..088624a1 100644 --- a/pkg/snclient/builtin_check.go +++ b/pkg/snclient/builtin_check.go @@ -55,6 +55,7 @@ func (l *CheckBuiltin) Check(ctx context.Context, snc *Agent, check *CheckData, args := []string{} args = append(args, check.rawArgs...) + // if snclient is started with verbose arguments, pass them to internal check as well switch { case snc.flags.Verbose >= 3: args = append(args, "-vvv") diff --git a/pkg/snclient/check_ssh.go b/pkg/snclient/check_ssh.go new file mode 100644 index 00000000..2a7dd01a --- /dev/null +++ b/pkg/snclient/check_ssh.go @@ -0,0 +1,57 @@ +package snclient + +import ( + "context" + "fmt" + "io" + "runtime" + "slices" + + "github.com/consol-monitoring/snclient/pkg/check_tcp" +) + +func init() { + AvailableChecks["check_ssh"] = CheckEntry{"check_ssh", NewCheckSSH} +} + +func NewCheckSSH() CheckHandler { + return &CheckBuiltin{ + name: "check_ssh", + description: `Runs check_tcp with an SSH configururation to check for a running SSH server. +It basically wraps the plugin from https://github.com/taku-k/go-check-plugins/tree/master/check-tcp`, + check: checkSSH, + docTitle: `check_ssh`, + usage: `check_ssh []`, + exampleDefault: ` + check_ssh github.com +SSH OK - 0.234 seconds response time on github.com port 22 [SSH-2.0-8ad108e] | time=0.234029s;;;0.000000;10.000000 + + check_ssh --hostname github.com --warning 1 +SSH OK - 0.262 seconds response time on github.com port 22 [SSH-2.0-8ad108e] | time=0.262048s;;;1.000000;10.000000 + `, + exampleArgs: `'-H' '192.168.178.100' '-p' '2323'`, + } +} + +func checkSSH(ctx context.Context, output io.Writer, args []string) int { + // snclient supports short arguments with multiple chars like -v and not -vv or -vvv + // if snclient agent has these verbose arguments, they are passed as is to internal checks + // if that is the case, delete them and add -v instead. + hadVerbose := false + args = slices.DeleteFunc(args, func(s string) bool { + isVerbose := s == "-v" || s == "-vv" || s == "-vvv" + if isVerbose { + hadVerbose = true + } + + return isVerbose + }) + if hadVerbose { + args = append(args, "-v") + } + + // the string to be sent on the connection + sendStr := fmt.Sprintf("SSH-1.0-snclient_build_%s_runtime_%s", Build, runtime.Version()) + + return check_tcp.CheckSSH(ctx, output, args, sendStr) +} diff --git a/pkg/snclient/check_ssh_test.go b/pkg/snclient/check_ssh_test.go new file mode 100644 index 00000000..2952c9f7 --- /dev/null +++ b/pkg/snclient/check_ssh_test.go @@ -0,0 +1,35 @@ +package snclient + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckSSH(t *testing.T) { + config := ` +[/modules] +CheckBuiltinPlugins = enabled +` + snc := StartTestAgent(t, config) + + res := snc.RunCheck("check_ssh", []string{"-H", "github.com", "-p", "22"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^SSH OK - [\d.]+ seconds response time on github.com port 22`, + string(res.BuildPluginOutput()), + "output matches", + ) + + res = snc.RunCheck("check_ssh", []string{"-H", "bitbucket.org"}) + assert.Equalf(t, CheckExitOK, res.State, "state ok") + assert.Regexpf( + t, + `^SSH OK - [\d.]+ seconds response time on bitbucket.org port 22`, + string(res.BuildPluginOutput()), + "output matches", + ) + + StopTestAgent(t, snc) +} diff --git a/pkg/snclient/check_tcp_test.go b/pkg/snclient/check_tcp_test.go index 6ae39a8a..a67d2ae9 100644 --- a/pkg/snclient/check_tcp_test.go +++ b/pkg/snclient/check_tcp_test.go @@ -20,7 +20,8 @@ use ssl = false res := snc.RunCheck("check_tcp", []string{"-H", "localhost", "-p", "45666"}) assert.Equalf(t, CheckExitOK, res.State, "state ok") - assert.Regexpf(t, + assert.Regexpf( + t, `^TCP OK - [\d.]+ seconds response time on localhost port 45666`, string(res.BuildPluginOutput()), "output matches",