check_ssh: patches from op5 (#1738)

* check_ssh: properly parse a delayed version control string

This resolves an issue with SSH servers which do not respond with their
version control string as the first thing in the SSH protocol version
exchange phase after connection establishment.

This patch also makes sure that we disregard a potential comment in the
version exchange string to avoid nonsense mismatches. In the future, we
might want to add the capability to match against a user specified comment.

In addition, the patch largely improves the communication towards the
server, which adds better protocol adherence.

Of course, new test cases are added to support the trigger and guard
against regressions of the bugs solved by this patch.

This fixes op5#7945 (https://bugs.op5.com/view.php?id=7945)

Signed-off-by: Anton Lofgren <alofgren@op5.com>

* check_ssh.t: Fix a few typos

Signed-off-by: Anton Lofgren <alofgren@op5.com>

* check_ssh: Handle non-alpha software versions

This patch fixes a bug where we would reject version control strings
that do not contain letters, because the assumption is made that they
always do. This is not required by the RFC however, and there exist
implementations that do not contain letters.

I've also added a few references to the RFC to make the process of
parsing the control string more apparent.

This fixes op5#8716 (https://bugs.op5.com/view.php?id=8716)

Signed-off-by: Anton Lofgren <alofgren@op5.com>

* check_ssh: Fix a typo in "remote-protocol parameter

remote-protcol -> remote-protocol

Signed-off-by: Anton Lofgren <alofgren@op5.com>

* Remove unused variable

* Formating fixes

* Update translations

* Remove merge conflict artefact from previous merge

* Set fixed include paths

* Improve code style to be slightly more readable

* Update test cases for different netcat behaviour and reduce sleep time

---------

Signed-off-by: Anton Lofgren <alofgren@op5.com>
Co-authored-by: Anton Lofgren <alofgren@op5.com>
This commit is contained in:
Lorenz Kästle 2024-03-27 00:35:16 +01:00 committed by GitHub
parent 8698a6d976
commit 66f62dd336
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 237 additions and 79 deletions

View file

@ -1,39 +1,39 @@
/*****************************************************************************
*
*
* Monitoring check_ssh plugin
*
*
* License: GPL
* Copyright (c) 2000-2007 Monitoring Plugins Development Team
*
*
* Description:
*
*
* This file contains the check_ssh plugin
*
*
* Try to connect to an SSH server at specified server and port
*
*
*
*
* 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 <http://www.gnu.org/licenses/>.
*
*
*
*
*****************************************************************************/
const char *progname = "check_ssh";
const char *copyright = "2000-2007";
const char *email = "devel@monitoring-plugins.org";
#include "common.h"
#include "netutils.h"
#include "./common.h"
#include "./netutils.h"
#include "utils.h"
#ifndef MSG_DONTWAIT
@ -105,7 +105,7 @@ process_arguments (int argc, char **argv)
{"timeout", required_argument, 0, 't'},
{"verbose", no_argument, 0, 'v'},
{"remote-version", required_argument, 0, 'r'},
{"remote-protcol", required_argument, 0, 'P'},
{"remote-protocol", required_argument, 0, 'P'},
{0, 0, 0, 0}
};
@ -214,7 +214,9 @@ ssh_connect (char *haddr, int hport, char *remote_version, char *remote_protocol
{
int sd;
int result;
char *output = NULL;
int len = 0;
ssize_t recv_ret = 0;
char *version_control_string = NULL;
char *buffer = NULL;
char *ssh_proto = NULL;
char *ssh_server = NULL;
@ -229,52 +231,126 @@ ssh_connect (char *haddr, int hport, char *remote_version, char *remote_protocol
if (result != STATE_OK)
return result;
output = (char *) malloc (BUFF_SZ + 1);
memset (output, 0, BUFF_SZ + 1);
recv (sd, output, BUFF_SZ, 0);
if (strncmp (output, "SSH", 3)) {
printf (_("Server answer: %s"), output);
char *output = (char *) calloc (BUFF_SZ + 1, sizeof(char));
unsigned int iteration = 0;
ssize_t byte_offset = 0;
while ((version_control_string == NULL) && (recv_ret = recv(sd, output+byte_offset, BUFF_SZ - byte_offset, 0) > 0)) {
if (strchr(output, '\n')) { /* we've got at least one full line, start parsing*/
byte_offset = 0;
char *index = NULL;
while ((index = strchr(output+byte_offset, '\n')) != NULL) {
/*Partition the buffer so that this line is a separate string,
* by replacing the newline with NUL*/
output[(index - output)] = '\0';
len = strlen(output + byte_offset);
if ((len >= 4) && (strncmp (output+byte_offset, "SSH-", 4) == 0)) {
/*if the string starts with SSH-, this _should_ be a valid version control string*/
version_control_string = output+byte_offset;
break;
}
/*the start of the next line (if one exists) will be after the current one (+ NUL)*/
byte_offset += (len + 1);
}
if(version_control_string == NULL) {
/* move unconsumed data to beginning of buffer, null rest */
memmove((void *)output, (void *)output+byte_offset+1, BUFF_SZ - len+1);
memset(output+byte_offset+1, 0, BUFF_SZ-byte_offset+1);
/*start reading from end of current line chunk on next recv*/
byte_offset = strlen(output);
}
} else {
byte_offset += recv_ret;
}
}
if (recv_ret < 0) {
printf("SSH CRITICAL - %s", strerror(errno));
exit(STATE_CRITICAL);
}
if (version_control_string == NULL) {
printf("SSH CRITICAL - No version control string received");
exit(STATE_CRITICAL);
}
/*
* "When the connection has been established, both sides MUST send an
* identification string. This identification string MUST be
*
* SSH-protoversion-softwareversion SP comments CR LF"
* - RFC 4253:4.2
*/
strip (version_control_string);
if (verbose)
printf ("%s\n", version_control_string);
ssh_proto = version_control_string + 4;
/*
* We assume the protoversion is of the form Major.Minor, although
* this is not _strictly_ required. See
*
* "Both the 'protoversion' and 'softwareversion' strings MUST consist of
* printable US-ASCII characters, with the exception of whitespace
* characters and the minus sign (-)"
* - RFC 4253:4.2
* and,
*
* "As stated earlier, the 'protoversion' specified for this protocol is
* "2.0". Earlier versions of this protocol have not been formally
* documented, but it is widely known that they use 'protoversion' of
* "1.x" (e.g., "1.5" or "1.3")."
* - RFC 4253:5
*/
ssh_server = ssh_proto + strspn (ssh_proto, "0123456789.") + 1; /* (+1 for the '-' separating protoversion from softwareversion) */
/* If there's a space in the version string, whatever's after the space is a comment
* (which is NOT part of the server name/version)*/
char *tmp = strchr(ssh_server, ' ');
if (tmp) {
ssh_server[tmp - ssh_server] = '\0';
}
if (strlen(ssh_proto) == 0 || strlen(ssh_server) == 0) {
printf(_("SSH CRITICAL - Invalid protocol version control string %s\n"), version_control_string);
exit (STATE_CRITICAL);
}
ssh_proto[strspn (ssh_proto, "0123456789. ")] = 0;
xasprintf (&buffer, "SSH-%s-check_ssh_%s\r\n", ssh_proto, rev_no);
send (sd, buffer, strlen (buffer), MSG_DONTWAIT);
if (verbose)
printf ("%s\n", buffer);
if (remote_version && strcmp(remote_version, ssh_server)) {
printf
(_("SSH CRITICAL - %s (protocol %s) version mismatch, expected '%s'\n"),
ssh_server, ssh_proto, remote_version);
close(sd);
exit (STATE_CRITICAL);
}
else {
strip (output);
if (verbose)
printf ("%s\n", output);
ssh_proto = output + 4;
ssh_server = ssh_proto + strspn (ssh_proto, "-0123456789. ");
ssh_proto[strspn (ssh_proto, "0123456789. ")] = 0;
xasprintf (&buffer, "SSH-%s-check_ssh_%s\r\n", ssh_proto, rev_no);
send (sd, buffer, strlen (buffer), MSG_DONTWAIT);
if (verbose)
printf ("%s\n", buffer);
if (remote_version && strcmp(remote_version, ssh_server)) {
printf
(_("SSH CRITICAL - %s (protocol %s) version mismatch, expected '%s'\n"),
ssh_server, ssh_proto, remote_version);
close(sd);
exit (STATE_CRITICAL);
}
if (remote_protocol && strcmp(remote_protocol, ssh_proto)) {
printf
(_("SSH CRITICAL - %s (protocol %s) protocol version mismatch, expected '%s'\n"),
ssh_server, ssh_proto, remote_protocol);
close(sd);
exit (STATE_CRITICAL);
}
elapsed_time = (double)deltime(tv) / 1.0e6;
if (remote_protocol && strcmp(remote_protocol, ssh_proto)) {
printf
(_("SSH OK - %s (protocol %s) | %s\n"),
ssh_server, ssh_proto, fperfdata("time", elapsed_time, "s",
(_("SSH CRITICAL - %s (protocol %s) protocol version mismatch, expected '%s' | %s\n"),
ssh_server, ssh_proto, remote_protocol, fperfdata("time", elapsed_time, "s",
false, 0, false, 0, true, 0, true, (int)socket_timeout));
close(sd);
exit (STATE_OK);
exit (STATE_CRITICAL);
}
elapsed_time = (double)deltime(tv) / 1.0e6;
printf
(_("SSH OK - %s (protocol %s) | %s\n"),
ssh_server, ssh_proto, fperfdata("time", elapsed_time, "s",
false, 0, false, 0, true, 0, true, (int)socket_timeout));
close(sd);
exit (STATE_OK);
}
@ -292,7 +368,7 @@ print_help (void)
printf ("%s\n", _("Try to connect to an SSH server at specified server and port"));
printf ("\n\n");
printf ("\n\n");
print_usage ();
@ -306,10 +382,10 @@ print_help (void)
printf (UT_CONN_TIMEOUT, DEFAULT_SOCKET_TIMEOUT);
printf (" %s\n", "-r, --remote-version=STRING");
printf (" %s\n", _("Alert if string doesn't match expected server version (ex: OpenSSH_3.9p1)"));
printf (" %s\n", _("Alert if string doesn't match expected server version (ex: OpenSSH_3.9p1)"));
printf (" %s\n", "-P, --remote-protocol=STRING");
printf (" %s\n", _("Alert if protocol doesn't match expected protocol version (ex: 2.0)"));
printf (" %s\n", _("Alert if protocol doesn't match expected protocol version (ex: 2.0)"));
printf (UT_VERBOSE);
@ -321,7 +397,7 @@ print_help (void)
void
print_usage (void)
{
printf ("%s\n", _("Usage:"));
printf ("%s\n", _("Usage:"));
printf ("%s [-4|-6] [-t <timeout>] [-r <remote version>] [-p <port>] <host>\n", progname);
}

View file

@ -8,34 +8,116 @@ use strict;
use Test::More;
use NPTest;
my $res;
# Required parameters
my $ssh_host = getTestParameter("NP_SSH_HOST", "A host providing SSH service", "localhost");
my $host_nonresponsive = getTestParameter("NP_HOST_NONRESPONSIVE", "The hostname of system not responsive to network requests", "10.0.0.1" );
my $hostname_invalid = getTestParameter("NP_HOSTNAME_INVALID", "An invalid (not known to DNS) hostname", "nosuchhost" );
my $ssh_host = getTestParameter("NP_SSH_HOST",
"A host providing SSH service",
"localhost");
my $host_nonresponsive = getTestParameter("NP_HOST_NONRESPONSIVE",
"The hostname of system not responsive to network requests",
"10.0.0.1" );
my $hostname_invalid = getTestParameter("NP_HOSTNAME_INVALID",
"An invalid (not known to DNS) hostname",
"nosuchhost" );
plan skip_all => "SSH_HOST must be defined" unless $ssh_host;
plan tests => 6;
plan tests => 14 + 6;
SKIP: {
skip "SSH_HOST must be defined", 6 unless $ssh_host;
my $result = NPTest->testCmd(
"./check_ssh -H $ssh_host"
);
cmp_ok($result->return_code, '==', 0, "Exit with return code 0 (OK)");
like($result->output, '/^SSH OK - /', "Status text if command returned none (OK)");
my $result = NPTest->testCmd(
"./check_ssh -H $ssh_host"
);
cmp_ok($result->return_code, '==', 0, "Exit with return code 0 (OK)");
like($result->output, '/^SSH OK - /', "Status text if command returned none (OK)");
$result = NPTest->testCmd(
"./check_ssh -H $host_nonresponsive -t 2"
);
cmp_ok($result->return_code, '==', 2, "Exit with return code 0 (OK)");
like($result->output, '/^CRITICAL - Socket timeout after 2 seconds/', "Status text if command returned none (OK)");
$result = NPTest->testCmd(
"./check_ssh -H $host_nonresponsive -t 2"
);
cmp_ok($result->return_code, '==', 2, "Exit with return code 0 (OK)");
like($result->output, '/^CRITICAL - Socket timeout after 2 seconds/', "Status text if command returned none (OK)");
$result = NPTest->testCmd(
"./check_ssh -H $hostname_invalid -t 2"
);
cmp_ok($result->return_code, '==', 3, "Exit with return code 0 (OK)");
like($result->output, '/^check_ssh: Invalid hostname/', "Status text if command returned none (OK)");
$result = NPTest->testCmd(
"./check_ssh -H $hostname_invalid -t 2"
);
cmp_ok($result->return_code, '==', 3, "Exit with return code 0 (OK)");
like($result->output, '/^check_ssh: Invalid hostname/', "Status text if command returned none (OK)");
}
SKIP: {
skip "No netcat available", 14 unless (system("which nc > /dev/null") == 0);
# netcat on linux (on debian) will just keep the socket open if not advised otherwise
# therefore we add -q to close it after two seconds after receiving the EOF from input
my $nc_flags = "-l 5003 -N";
#A valid protocol version control string has the form
# SSH-protoversion-softwareversion SP comments CR LF
#
# where `comments` is optional, protoversion is the SSH protocol version and
# softwareversion is an arbitrary string representing the server software version
open(NC, "echo 'SSH-2.0-nagiosplug.ssh.0.1' | nc ${nc_flags}|");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003" );
cmp_ok( $res->return_code, '==', 0, "Got SSH protocol version control string");
like( $res->output, '/^SSH OK - nagiosplug.ssh.0.1 \(protocol 2.0\)/', "Output OK");
close NC;
open(NC, "echo 'SSH-2.0-3.2.9.1' | nc ${nc_flags}|");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003" );
cmp_ok( $res->return_code, "==", 0, "Got SSH protocol version control string with non-alpha softwareversion string");
like( $res->output, '/^SSH OK - 3.2.9.1 \(protocol 2.0\)/', "Output OK for non-alpha softwareversion string");
close NC;
open(NC, "echo 'SSH-2.0-nagiosplug.ssh.0.1 this is a comment' | nc ${nc_flags} |");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003 -r nagiosplug.ssh.0.1" );
cmp_ok( $res->return_code, '==', 0, "Got SSH protocol version control string, and parsed comment appropriately");
like( $res->output, '/^SSH OK - nagiosplug.ssh.0.1 \(protocol 2.0\)/', "Output OK");
close NC;
open(NC, "echo 'SSH-' | nc ${nc_flags}|");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003" );
cmp_ok( $res->return_code, '==', 2, "Got invalid SSH protocol version control string");
like( $res->output, '/^SSH CRITICAL/', "Output OK");
close NC;
open(NC, "echo '' | nc ${nc_flags}|");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003" );
cmp_ok( $res->return_code, '==', 2, "No version control string received");
like( $res->output, '/^SSH CRITICAL - No version control string received/', "Output OK");
close NC;
open(NC, "echo 'Not a version control string' | nc ${nc_flags}|");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003" );
cmp_ok( $res->return_code, '==', 2, "No version control string received");
like( $res->output, '/^SSH CRITICAL - No version control string received/', "Output OK");
close NC;
#RFC 4253 permits servers to send any number of data lines prior to sending the protocol version control string
open(NC, "{ echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; sleep 0.5;
echo 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'; sleep 0.5;
echo 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'; sleep 0.2;
echo 'DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'; sleep 0.3;
printf 'EEEEEEEEEEEEEEEEEE'; sleep 0.2;
printf 'EEEEEEEEEEEEEEEEEE\n'; sleep 0.2;
echo 'Some\nPrepended\nData\nLines\n'; sleep 0.2;
echo 'SSH-2.0-nagiosplug.ssh.0.2';} | nc ${nc_flags}|");
sleep 0.1;
$res = NPTest->testCmd( "./check_ssh -H localhost -p 5003" );
cmp_ok( $res->return_code, '==', 0, "Got delayed SSH protocol version control string");
like( $res->output, '/^SSH OK - nagiosplug.ssh.0.2 \(protocol 2.0\)/', "Output OK");
close NC;
}