]>
Commit | Line | Data |
---|---|---|
bae5c928 MT |
1 | #!/usr/bin/python |
2 | ||
3 | from __future__ import division | |
4 | ||
5 | import array | |
6 | import math | |
7 | import os | |
8 | import random | |
9 | import select | |
10 | import socket | |
11 | import struct | |
12 | import sys | |
13 | import time | |
14 | ||
15 | ICMP_TYPE_ECHO_REPLY = 0 | |
16 | ICMP_TYPE_ECHO_REQUEST = 8 | |
17 | ICMP_MAX_RECV = 2048 | |
18 | ||
19 | MAX_SLEEP = 1000 | |
20 | ||
21 | class PingError(Exception): | |
22 | msg = None | |
23 | ||
24 | ||
25 | class PingResolveError(PingError): | |
26 | msg = "Could not resolve hostname" | |
27 | ||
28 | ||
29 | class Ping(object): | |
30 | def __init__(self, destination, timeout=1000, packet_size=56): | |
31 | self.destination = self._resolve(destination) | |
32 | self.timeout = timeout | |
33 | self.packet_size = packet_size | |
34 | ||
35 | self.id = os.getpid() & 0xffff # XXX ? Is this a good idea? | |
36 | ||
37 | self.seq_number = 0 | |
38 | ||
39 | # Number of sent packets. | |
40 | self.send_count = 0 | |
41 | ||
42 | # Save the delay of all responses. | |
43 | self.times = [] | |
44 | ||
45 | def run(self, count=None, deadline=None): | |
46 | while True: | |
47 | delay = self.do() | |
48 | ||
49 | self.seq_number += 1 | |
50 | ||
51 | if count and self.seq_number >= count: | |
52 | break | |
53 | ||
54 | if deadline and self.total_time >= deadline: | |
55 | break | |
56 | ||
57 | if delay == None: | |
58 | delay = 0 | |
59 | ||
60 | if MAX_SLEEP > delay: | |
61 | time.sleep((MAX_SLEEP - delay) / 1000) | |
62 | ||
63 | def do(self): | |
64 | s = None | |
65 | try: | |
66 | # Open a socket for ICMP communication. | |
67 | s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp")) | |
68 | ||
69 | # Send one packet. | |
70 | send_time = self.send_icmp_echo_request(s) | |
71 | ||
72 | # Increase number of sent packets (even if it could not be sent). | |
73 | self.send_count += 1 | |
74 | ||
75 | # If the packet could not be sent, we may stop here. | |
76 | if send_time is None: | |
77 | return | |
78 | ||
79 | # Wait for the reply. | |
80 | receive_time, packet_size, ip, ip_header, icmp_header = self.receive_icmp_echo_reply(s) | |
81 | ||
82 | finally: | |
83 | # Close the socket. | |
84 | if s: | |
85 | s.close() | |
86 | ||
87 | # If a packet has been received... | |
88 | if receive_time: | |
89 | delay = (receive_time - send_time) * 1000 | |
90 | self.times.append(delay) | |
91 | ||
92 | return delay | |
93 | ||
94 | def send_icmp_echo_request(self, s): | |
95 | # Header is type (8), code (8), checksum (16), id (16), sequence (16) | |
96 | checksum = 0 | |
97 | ||
98 | # Create a header with checksum == 0. | |
99 | header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0, | |
100 | checksum, self.id, self.seq_number) | |
101 | ||
102 | # Get some bytes for padding. | |
103 | padding = os.urandom(self.packet_size) | |
104 | ||
105 | # Calculate the checksum for header + padding data. | |
106 | checksum = self._calculate_checksum(header + padding) | |
107 | ||
108 | # Rebuild the header with the new checksum. | |
109 | header = struct.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST, 0, | |
110 | checksum, self.id, self.seq_number) | |
111 | ||
112 | # Build the packet. | |
113 | packet = header + padding | |
114 | ||
115 | # Save the time when the packet has been sent. | |
116 | send_time = time.time() | |
117 | ||
118 | # Send the packet. | |
119 | try: | |
120 | s.sendto(packet, (self.destination, 0)) | |
121 | except socket.error, (errno, msg): | |
122 | if errno == 1: # Operation not permitted | |
123 | # The packet could not be sent, probably because of | |
124 | # wrong firewall settings. | |
125 | return | |
126 | ||
127 | return send_time | |
128 | ||
129 | def receive_icmp_echo_reply(self, s): | |
130 | timeout = self.timeout / 1000.0 | |
131 | ||
132 | # Wait until the reply packet arrived or until we hit timeout. | |
133 | while True: | |
134 | select_start = time.time() | |
135 | ||
136 | inputready, outputready, exceptready = select.select([s], [], [], timeout) | |
137 | select_duration = (time.time() - select_start) | |
138 | ||
139 | if inputready == []: # Timeout | |
140 | return None, 0, 0, 0, 0 | |
141 | ||
142 | # Save the time when the packet has been received. | |
143 | receive_time = time.time() | |
144 | ||
145 | # Read the packet from the socket. | |
146 | packet_data, address = s.recvfrom(ICMP_MAX_RECV) | |
147 | ||
148 | # Parse the ICMP header. | |
149 | icmp_header = self._header2dict( | |
150 | ["type", "code", "checksum", "packet_id", "seq_number"], | |
151 | "!BBHHH", packet_data[20:28] | |
152 | ) | |
153 | ||
154 | # This is the reply to our packet if the ID matches. | |
155 | if icmp_header["packet_id"] == self.id: | |
156 | # Parse the IP header. | |
157 | ip_header = self._header2dict( | |
158 | ["version", "type", "length", "id", "flags", | |
159 | "ttl", "protocol", "checksum", "src_ip", "dst_ip"], | |
160 | "!BBHHHBBHII", packet_data[:20] | |
161 | ) | |
162 | ||
163 | packet_size = len(packet_data) - 28 | |
164 | ip = socket.inet_ntoa(struct.pack("!I", ip_header["src_ip"])) | |
165 | ||
166 | return receive_time, packet_size, ip, ip_header, icmp_header | |
167 | ||
168 | # Check if the timeout has already been hit. | |
169 | timeout = timeout - select_duration | |
170 | if timeout <= 0: | |
171 | return None, 0, 0, 0, 0 | |
172 | ||
173 | def _header2dict(self, names, struct_format, data): | |
174 | """ | |
175 | Unpack tghe raw received IP and ICMP header informations to a dict | |
176 | """ | |
177 | unpacked_data = struct.unpack(struct_format, data) | |
178 | return dict(zip(names, unpacked_data)) | |
179 | ||
180 | def _calculate_checksum(self, source_string): | |
181 | if len(source_string) % 2: | |
182 | source_string += "\x00" | |
183 | ||
184 | converted = array.array("H", source_string) | |
185 | if sys.byteorder == "big": | |
186 | converted.byteswap() | |
187 | ||
188 | val = sum(converted) | |
189 | ||
190 | # Truncate val to 32 bits (a variance from ping.c, which uses signed | |
191 | # integers, but overflow is unlinkely in ping). | |
192 | val &= 0xffffffff | |
193 | ||
194 | # Add high 16 bits to low 16 bits. | |
195 | val = (val >> 16) + (val & 0xffff) | |
196 | ||
197 | # Add carry from above (if any). | |
198 | val += (val >> 16) | |
199 | ||
200 | # Invert and truncate to 16 bits. | |
201 | answer = ~val & 0xffff | |
202 | ||
203 | return socket.htons(answer) | |
204 | ||
205 | def _resolve(self, host): | |
206 | """ | |
207 | Resolve host. | |
208 | """ | |
209 | if self._is_valid_ipv4_address(host): | |
210 | return host | |
211 | ||
212 | try: | |
213 | return socket.gethostbyname(host) | |
214 | except PingResolvError: | |
215 | raise PingResolveError | |
216 | ||
217 | def _is_valid_ipv4_address(self, addr): | |
218 | """ | |
219 | Check addr to be a valid IPv4 address. | |
220 | """ | |
221 | parts = addr.split(".") | |
222 | ||
223 | if not len(parts) == 4: | |
224 | return False | |
225 | ||
226 | for part in parts: | |
227 | try: | |
228 | number = int(part) | |
229 | except ValueError: | |
230 | return False | |
231 | ||
232 | if number > 255: | |
233 | return False | |
234 | ||
235 | return True | |
236 | ||
237 | @property | |
238 | def receive_count(self): | |
239 | """ | |
240 | The number of received packets. | |
241 | """ | |
242 | return len(self.times) | |
243 | ||
244 | @property | |
245 | def total_time(self): | |
246 | """ | |
247 | The total time of all roundtrips. | |
248 | """ | |
249 | try: | |
250 | return sum(self.times) | |
251 | except ValueError: | |
252 | return | |
253 | ||
254 | @property | |
255 | def min_time(self): | |
256 | """ | |
257 | The smallest roundtrip time. | |
258 | """ | |
259 | try: | |
260 | return min(self.times) | |
261 | except ValueError: | |
262 | return | |
263 | ||
264 | @property | |
265 | def max_time(self): | |
266 | """ | |
267 | The biggest roundtrip time. | |
268 | """ | |
269 | try: | |
270 | return max(self.times) | |
271 | except ValueError: | |
272 | return | |
273 | ||
274 | @property | |
275 | def avg_time(self): | |
276 | """ | |
277 | Calculate the average response time. | |
278 | """ | |
279 | try: | |
280 | return self.total_time / self.receive_count | |
281 | except ZeroDivisionError: | |
282 | return | |
283 | ||
284 | @property | |
285 | def variance(self): | |
286 | """ | |
287 | Calculate the variance of all roundtrips. | |
288 | """ | |
289 | if self.avg_time is None: | |
290 | return | |
291 | ||
292 | var = 0 | |
293 | ||
294 | for t in self.times: | |
295 | var += (t - self.avg_time) ** 2 | |
296 | ||
297 | var /= self.receive_count | |
298 | return var | |
299 | ||
300 | @property | |
301 | def stddev(self): | |
302 | """ | |
303 | Standard deviation of all roundtrips. | |
304 | """ | |
305 | return math.sqrt(self.variance) | |
306 | ||
307 | @property | |
308 | def loss(self): | |
309 | """ | |
310 | Outputs the percentage of dropped packets. | |
311 | """ | |
312 | dropped = self.send_count - self.receive_count | |
313 | ||
314 | return dropped / self.send_count | |
315 | ||
316 | ||
317 | if __name__ == "__main__": | |
318 | p = Ping("ping.ipfire.org") | |
319 | p.run(count=5) | |
320 | ||
321 | print "Min/Avg/Max/Stddev: %.2f/%.2f/%.2f/%.2f" % \ | |
322 | (p.min_time, p.avg_time, p.max_time, p.stddev) | |
323 | print "Sent/Recv/Loss: %d/%d/%.2f" % (p.send_count, p.receive_count, p.loss) |