#!/usr/bin/env python # This Nagios plugin may be used to check the health of an RDP server, such # as a Windows host offering remote desktop. Typically, a "strange" RDP # response is a good indication of a Windows host is having trouble (while # it is still responding to ping). # It seems that the RDP protocol is based on a protocol called X.224, # and this plugin only goes as far as checking very basic X.224 # protocol operations. Hence, the somewhat strange name of the plugin. # Example of a check command definition, using this plugin: # define command{ # command_name check_x224 # command_line /usr/local/nagios/check_x224 -H $HOSTADDRESS$ # } # # A corresponding service definition might look like: # define service{ # service_description Remote desktop # check_command check_x224 # host_name somename.example.com # use generic-service # } # Author: Troels Arvin # Versioning: # $Revision: 15676 $ # $Date: 2011-03-28 22:05:26 +0200 (Mon, 28 Mar 2011) $ # Copyright (c) 2011, Danish National Board of Health. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the the Danish National Board of Health nor the # names of its contributors may be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY the Danish National Board of Health ''AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL the Danish National Board of Health BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # References: # TPKT: http://www.itu.int/rec/T-REC-T.123/ # X.224: http://www.itu.int/rec/T-REC-X.224/en default_rdp_port = 3389 default_warning_sec = 3 default_critical_sec = 50 def do_conn(hostname,port,setup_payload,teardown_payload): try: s = socket.socket() t1 = time.time() # connect s.connect((hostname,port)) sent_bytes = s.send(setup_payload) if sent_bytes != len(setup_payload): print('Could not send RDP setup payload') sys.exit(2) setup_received = s.recv(1024) t2 = time.time() # disconnect sent_bytes = s.send(teardown_payload) if sent_bytes != len(teardown_payload): print('x224 CRITICAL: Could not send RDP teardown payload') sys.exit(2) s.close() elapsed = t2 - t1 l_setup_received = len(setup_received) l_expected_short = 11 l_expected_long = 19 if l_setup_received <> l_expected_short and l_setup_received <> l_expected_long: print('x224 CRITICAL: RDP response of unexpected length (%d)' % l_setup_received) sys.exit(2) except socket.error, e: if e[0] == -2: print("x224 UNKNOWN: Could not resolve hostname '%s': %s" % (hostname,e)) sys.exit(3) print('x224 CRITICAL: Could not set up connection on port %d: %s' % (port,e)) sys.exit(2) except Exception, e: print('x224 CRITICAL: Problem communicating with RDP server: %s' % e) sys.exit(2) return (elapsed,setup_received) # wrapping in gigantic try-block to be able to return 3 if something # unexpected goes wrong try: import os import sys import getopt import socket import struct import time this_script = os.path.basename(__file__) def usage(): print("""Usage: %s [-h|--help] -H hostname [-p|--port port] [-w|--warning seconds] [-c|--critical seconds] port : tcp port to connect to; default: %d warning seconds : number of seconds that an RDP response may take without emitting a warning; default: %d critical seconds: number of seconds that an RDP response may take without emitting status=critical; default: %d""" % (this_script,default_rdp_port,default_warning_sec,default_critical_sec)) sys.exit(3) try: options, args = getopt.getopt(sys.argv[1:], "hw:c:H:p:", [ 'help', 'warning=', 'critical=', 'port=' ] ) except getopt.GetoptError: usage() sys.exit(3) warning_sec = default_warning_sec critical_sec = default_critical_sec rdp_port = default_rdp_port hostname = '' for name, value in options: if name in ("-h", "--help"): usage() if name == '-H': hostname = value if name in ('-p', '--port'): try: rdp_port = int(value) except Exception: print("Unable to convert port to integer\n") usage() if name in ("-w", "--warning"): try: warning_sec = int(value) except Exception: print("Unable to convert warning_sec to integer\n") usage() if name in ("-c", "--critical"): try: critical_sec = int(value) except Exception: print("Unable to convert critical_sec to integer\n") usage() if rdp_port < 0: print('port number (%d) negative' % rdp_port) usage() if hostname == '': print('Hostname (-H) not indicated') usage() if (warning_sec > critical_sec): print('warning seconds (%d) may not be greater than critical_seconds (%d)' % (warning_sec,critical_sec)) usage() # make sure that we don't give up before critical sec has had a chance to elapse socket.setdefaulttimeout(critical_sec+2) setup_x224_cookie = "Cookie: mstshash=\r\n" setup_x224_rdp_neg_data = struct.pack( # little-endian here, it seems ? ' critical_sec: print('x224 CRITICAL: RDP connection setup time (%f) was longer than (%d) seconds' % (elapsed,critical_sec)) sys.exit(2) if elapsed > warning_sec: print('x224 WARNING: RDP connection setup time (%f) was longer than (%d) seconds' % (elapsed,warning_sec)) sys.exit(1) rec_tpkt_header={} rec_x224_header={} rec_nego_resp ={} # Older Windows hosts will return with a short answer if len(rec) == 11: rec_tpkt_header['version'], \ rec_tpkt_header['reserved'], \ rec_tpkt_header['length'], \ \ rec_x224_header['length'], \ rec_x224_header['code'], \ rec_x224_header['dst_ref'], \ rec_x224_header['src_ref'], \ rec_x224_header['class'], \ = struct.unpack('!BBHBBHHB',rec) else: # Newer Windows hosts will return with a longer answer rec_tpkt_header['version'], \ rec_tpkt_header['reserved'], \ rec_tpkt_header['length'], \ \ rec_x224_header['length'], \ rec_x224_header['code'], \ rec_x224_header['dst_ref'], \ rec_x224_header['src_ref'], \ rec_x224_header['class'], \ \ rec_nego_resp['type'], \ rec_nego_resp['flags'], \ rec_nego_resp['length'], \ rec_nego_resp['selected_proto'] \ = struct.unpack('!BBHBBHHBBBHI',rec) if rec_tpkt_header['version'] <> 3: print('x224 CRITICAL: Unexpected version-value(%d) in TPKT response' % rec_tpkt_header['version']) sys.exit(2) # 13 = binary 00001101; corresponding to 11010000 shifted four times # dst_ref=0 and class=0 was asked for in the connection setup if (rec_x224_header['code'] >> 4) <> 13 or \ rec_x224_header['dst_ref'] <> 0 or \ rec_x224_header['class'] <> 0: print('x224 CRITICAL: Unexpected element(s) in X.224 response') sys.exit(2) except struct.error, e: print('x224 CRITICAL: Could not decode RDP response: %s' % e) sys.exit(2) except SystemExit, e: # Special case which is needed in order to convert the return code # from other exception handlers. sys.exit(int(str(e))) except: # At this point, we don't know what's going on, so let's # not output the details of the error into something which # would appear in the Nagios web interface. print('x224 UNKNOWN: An unhandled error occurred') sys.stderr.write('Unhandled error: %s' % sys.exc_info()[1]) sys.exit(3) print('x224 OK. Connection setup time: %f sec.|time=%fs;%d;%d;0' % (elapsed,elapsed,warning_sec,critical_sec)) sys.exit(0)