| Index: third_party/pexpect/examples/topip.py
|
| diff --git a/third_party/pexpect/examples/topip.py b/third_party/pexpect/examples/topip.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..25bd100eb493eb51e4e2d593d3f1cbe841cf2099
|
| --- /dev/null
|
| +++ b/third_party/pexpect/examples/topip.py
|
| @@ -0,0 +1,299 @@
|
| +#!/usr/bin/env python
|
| +
|
| +''' This runs netstat on a local or remote server. It calculates some simple
|
| +statistical information on the number of external inet connections. It groups
|
| +by IP address. This can be used to detect if one IP address is taking up an
|
| +excessive number of connections. It can also send an email alert if a given IP
|
| +address exceeds a threshold between runs of the script. This script can be used
|
| +as a drop-in Munin plugin or it can be used stand-alone from cron. I used this
|
| +on a busy web server that would sometimes get hit with denial of service
|
| +attacks. This made it easy to see if a script was opening many multiple
|
| +connections. A typical browser would open fewer than 10 connections at once.
|
| +A script might open over 100 simultaneous connections.
|
| +
|
| +./topip.py [-s server_hostname] [-u username] [-p password]
|
| + {-a from_addr,to_addr} {-n N} {-v} {--ipv6}
|
| +
|
| + -s : hostname of the remote server to login to.
|
| + -u : username to user for login.
|
| + -p : password to user for login.
|
| + -n : print stddev for the the number of the top 'N' ipaddresses.
|
| + -v : verbose - print stats and list of top ipaddresses.
|
| + -a : send alert if stddev goes over 20.
|
| + -l : to log message to /var/log/topip.log
|
| + --ipv6 : this parses netstat output that includes ipv6 format.
|
| + Note that this actually only works with ipv4 addresses, but for
|
| + versions of netstat that print in ipv6 format.
|
| + --stdev=N : Where N is an integer. This sets the trigger point
|
| + for alerts and logs. Default is to trigger if the
|
| + max value is over 5 standard deviations.
|
| +
|
| +Example:
|
| +
|
| + This will print stats for the top IP addresses connected to the given host:
|
| +
|
| + ./topip.py -s www.example.com -u mylogin -p mypassword -n 10 -v
|
| +
|
| + This will send an alert email if the maxip goes over the stddev trigger
|
| + value and the the current top ip is the same as the last top ip
|
| + (/tmp/topip.last):
|
| +
|
| + ./topip.py -s www.example.com -u mylogin -p mypassword \\
|
| + -n 10 -v -a alert@example.com,user@example.com
|
| +
|
| + This will print the connection stats for the localhost in Munin format:
|
| +
|
| + ./topip.py
|
| +
|
| +PEXPECT LICENSE
|
| +
|
| + This license is approved by the OSI and FSF as GPL-compatible.
|
| + http://opensource.org/licenses/isc-license.txt
|
| +
|
| + Copyright (c) 2012, Noah Spurrier <noah@noah.org>
|
| + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY
|
| + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE
|
| + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES.
|
| + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
| + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
| + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
| + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
| + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
| + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
| + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
| +
|
| +'''
|
| +
|
| +from __future__ import absolute_import
|
| +from __future__ import print_function
|
| +from __future__ import unicode_literals
|
| +
|
| +# See http://pexpect.sourceforge.net/
|
| +import pexpect
|
| +import pxssh
|
| +import os
|
| +import sys
|
| +import time
|
| +import getopt
|
| +import pickle
|
| +import getpass
|
| +import smtplib
|
| +from pprint import pprint
|
| +
|
| +
|
| +try:
|
| + raw_input
|
| +except NameError:
|
| + raw_input = input
|
| +
|
| +
|
| +TOPIP_LOG_FILE = '/var/log/topip.log'
|
| +TOPIP_LAST_RUN_STATS = '/var/run/topip.last'
|
| +
|
| +def exit_with_usage():
|
| +
|
| + print(globals()['__doc__'])
|
| + os._exit(1)
|
| +
|
| +def stats(r):
|
| +
|
| + '''This returns a dict of the median, average, standard deviation,
|
| + min and max of the given sequence.
|
| +
|
| + >>> from topip import stats
|
| + >>> print stats([5,6,8,9])
|
| + {'med': 8, 'max': 9, 'avg': 7.0, 'stddev': 1.5811388300841898, 'min': 5}
|
| + >>> print stats([1000,1006,1008,1014])
|
| + {'med': 1008, 'max': 1014, 'avg': 1007.0, 'stddev': 5.0, 'min': 1000}
|
| + >>> print stats([1,3,4,5,18,16,4,3,3,5,13])
|
| + {'med': 4, 'max': 18, 'avg': 6.8181818181818183, 'stddev': 5.6216817577237475, 'min': 1}
|
| + >>> print stats([1,3,4,5,18,16,4,3,3,5,13,14,5,6,7,8,7,6,6,7,5,6,4,14,7])
|
| + {'med': 6, 'max': 18, 'avg': 7.0800000000000001, 'stddev': 4.3259218670706474, 'min': 1}
|
| + '''
|
| +
|
| + total = sum(r)
|
| + avg = float(total)/float(len(r))
|
| + sdsq = sum([(i-avg)**2 for i in r])
|
| + s = sorted(list(r))
|
| + return dict(list(zip(['med', 'avg', 'stddev', 'min', 'max'],
|
| + (s[len(s)//2], avg, (sdsq/len(r))**.5, min(r), max(r)))))
|
| +
|
| +def send_alert (message, subject, addr_from, addr_to, smtp_server='localhost'):
|
| +
|
| + '''This sends an email alert.
|
| + '''
|
| +
|
| + message = ( 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n'
|
| + % (addr_from, addr_to, subject) + message )
|
| + server = smtplib.SMTP(smtp_server)
|
| + server.sendmail(addr_from, addr_to, message)
|
| + server.quit()
|
| +
|
| +def main():
|
| +
|
| + # Parse the options, arguments, etc.
|
| + try:
|
| + optlist, args = getopt.getopt(sys.argv[1:],
|
| + 'h?valqs:u:p:n:', ['help','h','?','ipv6','stddev='])
|
| + except Exception as e:
|
| + print(str(e))
|
| + exit_with_usage()
|
| + options = dict(optlist)
|
| +
|
| + munin_flag = False
|
| + if len(args) > 0:
|
| + if args[0] == 'config':
|
| + print('graph_title Netstat Connections per IP')
|
| + print('graph_vlabel Socket connections per IP')
|
| + print('connections_max.label max')
|
| + print('connections_max.info Maximum number of connections per IP')
|
| + print('connections_avg.label avg')
|
| + print('connections_avg.info Average number of connections per IP')
|
| + print('connections_stddev.label stddev')
|
| + print('connections_stddev.info Standard deviation')
|
| + return 0
|
| + elif args[0] != '':
|
| + print(args, len(args))
|
| + return 0
|
| + exit_with_usage()
|
| + if [elem for elem in options if elem in ['-h','--h','-?','--?','--help']]:
|
| + print('Help:')
|
| + exit_with_usage()
|
| + if '-s' in options:
|
| + hostname = options['-s']
|
| + else:
|
| + # if host was not specified then assume localhost munin plugin.
|
| + munin_flag = True
|
| + hostname = 'localhost'
|
| + # If localhost then don't ask for username/password.
|
| + if hostname != 'localhost' and hostname != '127.0.0.1':
|
| + if '-u' in options:
|
| + username = options['-u']
|
| + else:
|
| + username = raw_input('username: ')
|
| + if '-p' in options:
|
| + password = options['-p']
|
| + else:
|
| + password = getpass.getpass('password: ')
|
| + use_localhost = False
|
| + else:
|
| + use_localhost = True
|
| +
|
| + if '-l' in options:
|
| + log_flag = True
|
| + else:
|
| + log_flag = False
|
| + if '-n' in options:
|
| + average_n = int(options['-n'])
|
| + else:
|
| + average_n = None
|
| + if '-v' in options:
|
| + verbose = True
|
| + else:
|
| + verbose = False
|
| + if '-a' in options:
|
| + alert_flag = True
|
| + (alert_addr_from, alert_addr_to) = tuple(options['-a'].split(','))
|
| + else:
|
| + alert_flag = False
|
| + if '--ipv6' in options:
|
| + ipv6_flag = True
|
| + else:
|
| + ipv6_flag = False
|
| + if '--stddev' in options:
|
| + stddev_trigger = float(options['--stddev'])
|
| + else:
|
| + stddev_trigger = 5
|
| +
|
| + if ipv6_flag:
|
| + netstat_pattern = '(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+::ffff:(\S+):(\S+)\s+.*?\r'
|
| + else:
|
| + netstat_pattern = '(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(?:::ffff:)*(\S+):(\S+)\s+.*?\r'
|
| + #netstat_pattern = '(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+):(\S+)\s+.*?\r'
|
| +
|
| + # run netstat (either locally or via SSH).
|
| + if use_localhost:
|
| + p = pexpect.spawn('netstat -n -t')
|
| + PROMPT = pexpect.TIMEOUT
|
| + else:
|
| + p = pxssh.pxssh()
|
| + p.login(hostname, username, password)
|
| + p.sendline('netstat -n -t')
|
| + PROMPT = p.PROMPT
|
| +
|
| + # For each matching netstat_pattern put the ip address in the list.
|
| + ip_list = {}
|
| + try:
|
| + while 1:
|
| + i = p.expect([PROMPT, netstat_pattern])
|
| + if i == 0:
|
| + break
|
| + k = p.match.groups()[4].decode('utf-8')
|
| + if k in ip_list:
|
| + ip_list[k] = ip_list[k] + 1
|
| + else:
|
| + ip_list[k] = 1
|
| + except:
|
| + pass
|
| +
|
| + # remove a few common, uninteresting addresses from the dictionary.
|
| + ip_list = dict([ (key,value) for key,value in ip_list.items() if '192.168.' not in key])
|
| + ip_list = dict([ (key,value) for key,value in ip_list.items() if '127.0.0.1' not in key])
|
| +
|
| + ip_list = list(ip_list.items())
|
| + if len(ip_list) < 1:
|
| + if verbose: print('Warning: no networks connections worth looking at.')
|
| + return 0
|
| + ip_list.sort(key=lambda x:x[1])
|
| +
|
| + # generate some stats for the ip addresses found.
|
| + if average_n is not None and average_n <= 1:
|
| + average_n = None
|
| + # Reminder: the * unary operator treats the list elements as arguments.
|
| + zipped = zip(*ip_list[0:average_n])
|
| + s = stats(list(zipped)[1])
|
| + s['maxip'] = ip_list[0]
|
| +
|
| + # print munin-style or verbose results for the stats.
|
| + if munin_flag:
|
| + print('connections_max.value', s['max'])
|
| + print('connections_avg.value', s['avg'])
|
| + print('connections_stddev.value', s['stddev'])
|
| + return 0
|
| + if verbose:
|
| + pprint (s)
|
| + print()
|
| + pprint (ip_list[0:average_n])
|
| +
|
| + # load the stats from the last run.
|
| + try:
|
| + last_stats = pickle.load(file(TOPIP_LAST_RUN_STATS))
|
| + except:
|
| + last_stats = {'maxip':None}
|
| +
|
| + if ( s['maxip'][1] > (s['stddev'] * stddev_trigger)
|
| + and s['maxip']==last_stats['maxip'] ):
|
| + if verbose: print('The maxip has been above trigger for two consecutive samples.')
|
| + if alert_flag:
|
| + if verbose: print('SENDING ALERT EMAIL')
|
| + send_alert(str(s), 'ALERT on %s'
|
| + % hostname, alert_addr_from, alert_addr_to)
|
| + if log_flag:
|
| + if verbose: print('LOGGING THIS EVENT')
|
| + fout = file(TOPIP_LOG_FILE,'a')
|
| + #dts = time.strftime('%Y:%m:%d:%H:%M:%S', time.localtime())
|
| + dts = time.asctime()
|
| + fout.write ('%s - %d connections from %s\n'
|
| + % (dts,s['maxip'][1],str(s['maxip'][0])))
|
| + fout.close()
|
| +
|
| + # save state to TOPIP_LAST_RUN_STATS
|
| + try:
|
| + pickle.dump(s, file(TOPIP_LAST_RUN_STATS,'w'))
|
| + os.chmod (TOPIP_LAST_RUN_STATS, 0o664)
|
| + except:
|
| + pass
|
| + # p.logout()
|
| +
|
| +if __name__ == '__main__':
|
| + main()
|
|
|