3 """ This runs netstat on a local or remote server. It calculates some simple
4 statistical information on the number of external inet connections. It groups
5 by IP address. This can be used to detect if one IP address is taking up an
6 excessive number of connections. It can also send an email alert if a given IP
7 address exceeds a threshold between runs of the script. This script can be used
8 as a drop-in Munin plugin or it can be used stand-alone from cron. I used this
9 on a busy web server that would sometimes get hit with denial of service
10 attacks. This made it easy to see if a script was opening many multiple
11 connections. A typical browser would open fewer than 10 connections at once. A
12 script might open over 100 simultaneous connections.
14 ./topip.py [-s server_hostname] [-u username] [-p password] {-a from_addr,to_addr} {-n N} {-v} {--ipv6}
16 -s : hostname of the remote server to login to.
17 -u : username to user for login.
18 -p : password to user for login.
19 -n : print stddev for the the number of the top 'N' ipaddresses.
20 -v : verbose - print stats and list of top ipaddresses.
21 -a : send alert if stddev goes over 20.
22 -l : to log message to /var/log/topip.log
23 --ipv6 : this parses netstat output that includes ipv6 format.
24 Note that this actually only works with ipv4 addresses, but for versions of
25 netstat that print in ipv6 format.
26 --stdev=N : Where N is an integer. This sets the trigger point for alerts and logs.
27 Default is to trigger if max value is above 5 standard deviations.
31 This will print stats for the top IP addresses connected to the given host:
33 ./topip.py -s www.example.com -u mylogin -p mypassword -n 10 -v
35 This will send an alert email if the maxip goes over the stddev trigger value and
36 the the current top ip is the same as the last top ip (/tmp/topip.last):
40 This will print the connection stats for the localhost in Munin format:
46 $Id: topip.py 489 2007-11-28 23:40:34Z noah $
49 import pexpect, pxssh # See http://pexpect.sourceforge.net/
50 import os, sys, time, re, getopt, pickle, getpass, smtplib
52 from pprint import pprint
54 TOPIP_LOG_FILE = '/var/log/topip.log'
55 TOPIP_LAST_RUN_STATS = '/var/run/topip.last'
57 def exit_with_usage():
59 print globals()['__doc__']
64 """This returns a dict of the median, average, standard deviation, min and max of the given sequence.
66 >>> from topip import stats
67 >>> print stats([5,6,8,9])
68 {'med': 8, 'max': 9, 'avg': 7.0, 'stddev': 1.5811388300841898, 'min': 5}
69 >>> print stats([1000,1006,1008,1014])
70 {'med': 1008, 'max': 1014, 'avg': 1007.0, 'stddev': 5.0, 'min': 1000}
71 >>> print stats([1,3,4,5,18,16,4,3,3,5,13])
72 {'med': 4, 'max': 18, 'avg': 6.8181818181818183, 'stddev': 5.6216817577237475, 'min': 1}
73 >>> 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])
74 {'med': 6, 'max': 18, 'avg': 7.0800000000000001, 'stddev': 4.3259218670706474, 'min': 1}
78 avg = float(total)/float(len(r))
79 sdsq = sum([(i-avg)**2 for i in r])
82 return dict(zip(['med', 'avg', 'stddev', 'min', 'max'] , (s[len(s)//2], avg, (sdsq/len(r))**.5, min(r), max(r))))
84 def send_alert (message, subject, addr_from, addr_to, smtp_server='localhost'):
86 """This sends an email alert.
89 message = 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n' % (addr_from, addr_to, subject) + message
90 server = smtplib.SMTP(smtp_server)
91 server.sendmail(addr_from, addr_to, message)
96 ######################################################################
97 ## Parse the options, arguments, etc.
98 ######################################################################
100 optlist, args = getopt.getopt(sys.argv[1:], 'h?valqs:u:p:n:', ['help','h','?','ipv6','stddev='])
104 options = dict(optlist)
108 if args[0] == 'config':
109 print 'graph_title Netstat Connections per IP'
110 print 'graph_vlabel Socket connections per IP'
111 print 'connections_max.label max'
112 print 'connections_max.info Maximum number of connections per IP'
113 print 'connections_avg.label avg'
114 print 'connections_avg.info Average number of connections per IP'
115 print 'connections_stddev.label stddev'
116 print 'connections_stddev.info Standard deviation'
119 print args, len(args)
122 if [elem for elem in options if elem in ['-h','--h','-?','--?','--help']]:
126 hostname = options['-s']
128 # if host was not specified then assume localhost munin plugin.
130 hostname = 'localhost'
131 # If localhost then don't ask for username/password.
132 if hostname != 'localhost' and hostname != '127.0.0.1':
134 username = options['-u']
136 username = raw_input('username: ')
138 password = options['-p']
140 password = getpass.getpass('password: ')
149 average_n = int(options['-n'])
158 (alert_addr_from, alert_addr_to) = tuple(options['-a'].split(','))
161 if '--ipv6' in options:
165 if '--stddev' in options:
166 stddev_trigger = float(options['--stddev'])
171 netstat_pattern = '(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+::ffff:(\S+):(\S+)\s+.*?\r'
173 netstat_pattern = '(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(?:::ffff:)*(\S+):(\S+)\s+.*?\r'
174 #netstat_pattern = '(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+):(\S+)\s+.*?\r'
176 # run netstat (either locally or via SSH).
178 p = pexpect.spawn('netstat -n -t')
179 PROMPT = pexpect.TIMEOUT
182 p.login(hostname, username, password)
183 p.sendline('netstat -n -t')
186 # loop through each matching netstat_pattern and put the ip address in the list.
190 i = p.expect([PROMPT, netstat_pattern])
193 k = p.match.groups()[4]
195 ip_list[k] = ip_list[k] + 1
201 # remove a few common, uninteresting addresses from the dictionary.
202 ip_list = dict([ (key,value) for key,value in ip_list.items() if '192.168.' not in key])
203 ip_list = dict([ (key,value) for key,value in ip_list.items() if '127.0.0.1' not in key])
205 # sort dict by value (count)
206 #ip_list = sorted(ip_list.iteritems(),lambda x,y:cmp(x[1], y[1]),reverse=True)
207 ip_list = ip_list.items()
209 if verbose: print 'Warning: no networks connections worth looking at.'
211 ip_list.sort(lambda x,y:cmp(y[1],x[1]))
213 # generate some stats for the ip addresses found.
216 s = stats(zip(*ip_list[0:average_n])[1]) # The * unary operator treats the list elements as arguments
217 s['maxip'] = ip_list[0]
219 # print munin-style or verbose results for the stats.
221 print 'connections_max.value', s['max']
222 print 'connections_avg.value', s['avg']
223 print 'connections_stddev.value', s['stddev']
228 pprint (ip_list[0:average_n])
230 # load the stats from the last run.
232 last_stats = pickle.load(file(TOPIP_LAST_RUN_STATS))
234 last_stats = {'maxip':None}
236 if s['maxip'][1] > (s['stddev'] * stddev_trigger) and s['maxip']==last_stats['maxip']:
237 if verbose: print 'The maxip has been above trigger for two consecutive samples.'
239 if verbose: print 'SENDING ALERT EMAIL'
240 send_alert(str(s), 'ALERT on %s' % hostname, alert_addr_from, alert_addr_to)
242 if verbose: print 'LOGGING THIS EVENT'
243 fout = file(TOPIP_LOG_FILE,'a')
244 #dts = time.strftime('%Y:%m:%d:%H:%M:%S', time.localtime())
246 fout.write ('%s - %d connections from %s\n' % (dts,s['maxip'][1],str(s['maxip'][0])))
249 # save state to TOPIP_LAST_RUN_STATS
251 pickle.dump(s, file(TOPIP_LAST_RUN_STATS,'w'))
252 os.chmod (TOPIP_LAST_RUN_STATS, 0664)
257 if __name__ == '__main__':
261 except SystemExit, e:
265 traceback.print_exc()