forked from henkpls/SIPPing
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sipping3.py
399 lines (343 loc) · 11.1 KB
/
sipping3.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
#!/usr/bin/python3
"""
======This version modified for Python3========
SIP Ping - A diagnostic utility for critical VoIP monitoring
Created by Daniel Thompson
Version 2.0
==========================================================================
Software License:
Do whatever you want with this code. There are no restrictions.
Not-license:
I'd like to hear back from you if you do something interesting with this.
==========================================================================
SIP Ping is a tool for monitoring a SIP gateway (PBX, SBC, phone) for deep
dive diagnostics. Most tools for VoIP monitoring are based on meeting SLA
figures and providing general "network availability" statistics. SIP Ping
is for granular troubleshooting.
See http://gekk.info/sipping for more information and suggested usage.
Commandline flags and defaults are available by running "python sipping.py -h"
"""
import sys
import random
import re
import os
import socket
import argparse
from datetime import datetime
import time
# handler for ctrl+c / SIGINT
# last action before quitting is to write a \n to the end of the output file
import signal
def signal_handler(sig, frame):
print("\nCtrl+C ({}:{})- exiting.".format(sig, frame))
if v_logpath != "*":
with open(v_logpath, "a", encoding="utf-8") as _f_log:
_f_log.write("\n")
printstats()
sys.exit(0)
def printstats():
# loss stats
print(("\t[Recd: {recd} | Lost: {lost}]".format(recd=v_recd, lost=v_lost)), end=" ")
if v_longest_run > 0:
print(("\t[loss stats:"), end=" ")
print(("longest run: " + str(v_longest_run)), end=" ")
if v_last_run_loss > 0:
print((" length of last run: " + str(v_last_run_loss)), end=" ")
if v_current_run_loss > 0:
print((" length of current run: " + str(v_current_run_loss)), end=" ")
print("]")
# min max avg
v_total = 0
for i in l_history:
v_total = v_total + i
if v_total > 0:
v_avg = v_total / len(l_history)
else:
v_avg = 0
print(
(
"\t[min/max/avg {min}/{max}/{avg}]".format(
min=v_min, max=v_max, avg=v_avg
)
)
)
# create and execute command line parser
parser = argparse.ArgumentParser(
description="Send SIP OPTIONS messages to a host and measure response time. Results are logged continuously to CSV."
)
parser.add_argument("host", help="Target SIP device to ping")
parser.add_argument(
"-I",
metavar="interval",
default=1000,
help="Interval in milliseconds between pings (default 1000)",
)
parser.add_argument(
"-u",
metavar="userid",
default="sipping",
help="User part of the From header (default sipping)",
)
parser.add_argument(
"-i",
metavar="ip",
default="*",
help="IP to send in the Via header (will TRY to get local IP by default)",
)
parser.add_argument(
"-d",
metavar="domain",
default="gekk.info",
help="Domain part of the From header (needed if your device filters based on domain)",
)
parser.add_argument(
"-p", metavar="port", default=5060, help="Destination port (default 5060)"
)
parser.add_argument(
"--ttl",
metavar="ttl",
default=70,
help="Value to use for the Max-Forwards field (default 70)",
)
parser.add_argument(
"-w",
metavar="file",
default="[[default]]",
help="File to write results to. (default sipping-logs/[ip] - * to disable.",
)
parser.add_argument(
"-t",
metavar="timeout",
default="1000",
help="Time (ms) to wait for response (default 1000)",
)
parser.add_argument(
"-c",
metavar="count",
default="0",
help="Number of pings to send (default infinite)",
)
parser.add_argument(
"-x", nargs="?", default=False, help="Print raw transmitted packets"
)
parser.add_argument("-X", nargs="?", default=False, help="Print raw received responses")
parser.add_argument(
"-q",
nargs="?",
default=True,
help="Do not print status messages (-x and -X ignore this)",
)
parser.add_argument("-S", nargs="?", default=True, help="Do not print loss statistics")
parser.add_argument(
"--rtt",
nargs="?",
default=False,
help="Only print rtt in ms on success, or 0.0 failure",
)
args = vars(parser.parse_args())
# populate data from commandline
# anything unspecified on the commandline is set to a default value by the parser
v_interval = int(args["I"])
v_fromip = args["i"]
v_sbc = args["host"]
v_userid = args["u"]
v_port = int(args["p"])
v_domain = args["d"]
v_ttl = args["ttl"]
v_timeout = int(args["t"])
v_rawsend = args["x"] is None
v_rawrecv = args["X"] is None
v_quiet = not args["q"]
v_nostats = not args["S"]
v_count = int(args["c"])
v_rtt = args["rtt"] is None
# did the user enter an IP?
if re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", v_sbc) is None:
# the user entered a hostname; resolve it
try:
v_sbc = socket.getaddrinfo(v_sbc, 5060)[0][4][0]
# socket.gaierror catches socket-specific exceptions but I think we can go without
# except socket.gaierror as error:
except Exception as exc:
if v_rtt:
print(0.0)
sys.exit(1)
# dns failure or no response
print(exc)
sys.exit(1)
if v_count == 0:
v_count = sys.maxsize
if args["w"] == "[[default]]":
if not os.path.exists("sipping-logs"):
os.mkdir("sipping-logs")
v_logpath = "sipping-logs/{ip}.csv".format(ip=v_sbc)
else:
v_logpath = args["w"]
# if log output is enabled, ensure CSV has header
if v_logpath != "*":
if not os.path.isfile(v_logpath):
# create new CSV file and write header
with open(v_logpath, "w", encoding="utf-8") as f_log:
f_log.write("time,timestamp,host,latency,callid,response")
def generate_nonce(length=8):
"""Generate pseudorandom number for call IDs."""
return "".join([str(random.randint(0, 9)) for i in range(length)])
# writes onscreen timestamps in a consistent format
def timef(timev=None):
if timev is None:
return datetime.now().strftime("%d/%m/%y %I:%M:%S:%f")
return datetime.fromtimestamp(timev)
# register signal handler for ctrl+c since we're ready to start
signal.signal(signal.SIGINT, signal_handler)
if not v_quiet:
print("Press Ctrl+C to abort")
# zero out statistics variables
v_recd = 0
v_lost = 0
v_longest_run = 0
v_last_run_loss = 0
v_current_run_loss = 0
last_lost = "never"
l_history = []
v_min = float("inf")
v_max = float("-inf")
v_iter = 0
# empty list of last 5 pings
l_current_results = []
# start the ping loop
##while 1:
while v_count > 0:
v_count -= 1
# create a socket
skt_sbc = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
skt_sbc.bind(("0.0.0.0", 0))
skt_sbc.settimeout(v_timeout / 1000.0)
# find out what port we're transmitting from to correlate the return packet
v_localport = skt_sbc.getsockname()[1]
# find out what IP we're sourcing from to populate the Via
if v_fromip != "*":
v_lanip = v_fromip
else:
v_lanip = socket.gethostbyname(socket.gethostname())
# latency is calculated from this timestamp
start = time.time()
# create a random callid so we can identify the message in a packet capture
v_callid = generate_nonce(length=10)
v_branch = generate_nonce(length=10)
# write the OPTIONS packet
v_register_one = """OPTIONS sip:{domain} SIP/2.0
Via: SIP/2.0/UDP {lanip}:{localport};branch=z9hG4bK{branch}
To: "SIP Ping"<sip:{userid}@{domain}>
From: "SIP Ping"<sip:{userid}@{domain}>
Call-ID: {callid}
CSeq: 1 OPTIONS
Max-forwards: {ttl}
X-redundancy: Request
Content-Length: 0
""".format(
domain=v_domain,
lanip=v_lanip,
userid=v_userid,
localport=v_localport,
callid=v_callid,
ttl=v_ttl,
branch=v_branch,
)
# print transmit announcement
if not v_quiet and not v_rtt:
print(
(
"> ({time}) Sending to {host}:{port} [id: {id}]".format(
host=v_sbc, port=v_port, time=timef(), id=v_callid
)
)
)
# if -x was passed, print the transmitted packet
if v_rawsend:
print(v_register_one)
# send the packet
skt_sbc.sendto(v_register_one.encode("utf-8"), (v_sbc, v_port))
start = time.time()
# wait for response
try:
# start a synchronous receive
data, addr = skt_sbc.recvfrom(1024) # buffer size is 1024 bytes
# latency is calculated against this time
end = time.time()
diff = float("%.2f" % ((end - start) * 1000.0))
# pick out the first line in order to get the SIP response code
v_response = data.split("\n".encode("utf-8"))[0]
# print success message and response code
if v_rtt:
print(diff)
elif not v_quiet:
print(
(
"< ({time}) Reply from {host} ({diff}ms): {response}".format(
host=addr[0], diff=diff, time=timef(), response=v_response
)
)
)
# if -X was passed, print the received packet
if v_rawrecv:
print(data)
# log success
l_current_results.append(
"{time},{timestamp},{host},{diff},{id},{response}".format(
host=addr[0],
diff=diff,
time=timef(),
timestamp=time.time(),
id=v_callid,
response=v_response,
)
)
# update statistics
l_history.append(diff)
if len(l_history) > 200:
l_history = l_history[1:]
v_min = min(v_min, diff)
v_max = max(v_max, diff)
v_recd = v_recd + 1
if v_current_run_loss > 0:
v_last_run_loss = v_current_run_loss
v_longest_run = max(v_longest_run, v_last_run_loss)
v_current_run_loss = 0
except socket.timeout:
# timed out; print a drop
if v_rtt:
print(0.0)
elif not v_quiet:
print(
(
"X ({time}) Timed out waiting for response from {host}".format(
host=v_sbc, time=timef()
)
)
)
# log a drop
l_current_results.append(
"{time},{timestamp},{host},drop,{id},drop".format(
host=v_sbc, time=timef(), timestamp=time.time(), id=v_callid
)
)
# increment statistics
v_lost = v_lost + 1
v_current_run_loss = v_current_run_loss + 1
v_iter = v_iter + 1
# if it's been five packets, print stats and write logfile
if v_iter > 4:
# print stats to screen
if not v_nostats:
printstats()
# if logging is enabled, append stats to logfile
if v_logpath != "*":
with open(v_logpath, "a", encoding="utf-8") as f_log:
f_log.write("\n" + ("\n".join(l_current_results)))
l_current_results = []
v_iter = 0
# pause for user-requested interval before sending next packet
if v_count > 0:
time.sleep(v_interval / 1000.0)
if v_lost > 0:
sys.exit(1)