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