Hi there, I've been digging around for an effective way of monitoring an RDP port that goes further than just checking that the port is open. Nagios runs a Python script that builds an x224 connection. I want to perform the same test with a Powershell script. I'm really looking to keep the solution simple - that is Powershell only, I'm not really interested in calling the Python script from Powershell or compiling the script. Does Powershell have the necessary libraries and would anyone be willing to rewrite the Python script below in Powershell? Thanks.
# coding=utf-8#!/usr/bin/env python3
# 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 <tra@sst.dk>
# Last modified: 2012-12-08.
# 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 as e:
if e.errno == -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 as 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".encode('ascii')
setup_x224_rdp_neg_data = struct.pack( # little-endian here, it seems ?
'<BBHI',
1, # type
0, # flags
8, # length
3, # TLS + CredSSP
)
setup_x224_header = struct.pack(
'!BBHHB',
len(setup_x224_cookie) + 6 + 8, # length, 1 byte
# 6: length of this header, excluding length byte
# 8: length of setup_x224_rdp_neg_data (static)
224, # code, 1 byte (224 = 0xe0 = connection request)
0, # dst-ref, 1 short
0, # src-ref, 1 short
0 # class, 1 byte
)
setup_x224 = setup_x224_header + setup_x224_cookie + setup_x224_rdp_neg_data
tpkt_total_len = len(setup_x224) + 4
# 4 is the static size of a tpkt header
setup_tpkt_header = struct.pack(
'!BBH',
3, # version, 1 byte
0, # reserved, 1 byte
tpkt_total_len # len, 1 short
)
setup_payload = setup_tpkt_header + setup_x224
#print('Len of cookie: %d' % len(setup_x224_cookie))
#print('Len of rdp_neg_data: %d' % len(setup_x224_rdp_neg_data))
#print('Len of header: %d' % len(setup_x224_header))
#print('Len of setup_x224: %d' % len(setup_x224))
#print('tpkt_total_len: %d' % tpkt_total_len)
teardown_payload = struct.pack(
'!BBHBBBBBBB',
3, # tpkt version, 1 byte
0, # tpkt reserved, 1 byte
11, # tpkt len, 1 short
6, # x224 len, 1 byte
128, # x224 code, 1 byte
0, # x224 ?, 1 byte
0, # x224 ?, 1 byte
0, # x224 ?, 1 byte
0, # x224 ?, 1 byte
0 # x224 ?, 1 byte
)
elapsed, rec = do_conn(hostname, rdp_port, setup_payload, teardown_payload)
if elapsed > 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 as e:
print('x224 CRITICAL: Could not decode RDP response: %s' % e)
sys.exit(2)
except SystemExit as e:
# Special case which is needed in order to convert the return code
# from other exception handlers.
sys.exit(int(str(e)))
except Exception as e:
# 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. Do print the details
# on stderr, though, to ease debugging.
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)