]>
git.ipfire.org Git - oddments/collecty.git/blob - src/collecty/ping.py
13 ICMP_TYPE_ECHO_REPLY
= 0
14 ICMP_TYPE_ECHO_REQUEST
= 8
19 class PingError(Exception):
23 class PingResolveError(PingError
):
24 msg
= "Could not resolve hostname"
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
33 self
.id = os
.getpid() & 0xffff # XXX ? Is this a good idea?
37 # Number of sent packets.
40 # Save the delay of all responses.
43 def run(self
, count
=None, deadline
=None):
49 if count
and self
.seq_number
>= count
:
52 if deadline
and self
.total_time
>= deadline
:
59 time
.sleep((MAX_SLEEP
- delay
) / 1000)
64 # Open a socket for ICMP communication.
65 s
= socket
.socket(socket
.AF_INET
, socket
.SOCK_RAW
, socket
.getprotobyname("icmp"))
68 send_time
= self
.send_icmp_echo_request(s
)
70 # Increase number of sent packets (even if it could not be sent).
73 # If the packet could not be sent, we may stop here.
78 receive_time
, packet_size
, ip
, ip_header
, icmp_header
= self
.receive_icmp_echo_reply(s
)
85 # If a packet has been received...
87 delay
= (receive_time
- send_time
) * 1000
88 self
.times
.append(delay
)
92 def send_icmp_echo_request(self
, s
):
93 # Header is type (8), code (8), checksum (16), id (16), sequence (16)
96 # Create a header with checksum == 0.
97 header
= struct
.pack("!BBHHH", ICMP_TYPE_ECHO_REQUEST
, 0,
98 checksum
, self
.id, self
.seq_number
)
100 # Get some bytes for padding.
101 padding
= os
.urandom(self
.packet_size
)
103 # Calculate the checksum for header + padding data.
104 checksum
= self
._calculate
_checksum
(header
+ padding
)
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
)
111 packet
= header
+ padding
113 # Save the time when the packet has been sent.
114 send_time
= time
.time()
118 s
.sendto(packet
, (self
.destination
, 0))
119 except socket
.error
as e
:
120 if e
.errno
== 1: # Operation not permitted
121 # The packet could not be sent, probably because of
122 # wrong firewall settings.
127 def receive_icmp_echo_reply(self
, s
):
128 timeout
= self
.timeout
/ 1000.0
130 # Wait until the reply packet arrived or until we hit timeout.
132 select_start
= time
.time()
134 inputready
, outputready
, exceptready
= select
.select([s
], [], [], timeout
)
135 select_duration
= (time
.time() - select_start
)
137 if inputready
== []: # Timeout
138 return None, 0, 0, 0, 0
140 # Save the time when the packet has been received.
141 receive_time
= time
.time()
143 # Read the packet from the socket.
144 packet_data
, address
= s
.recvfrom(ICMP_MAX_RECV
)
146 # Parse the ICMP header.
147 icmp_header
= self
._header
2dict
(
148 ["type", "code", "checksum", "packet_id", "seq_number"],
149 "!BBHHH", packet_data
[20:28]
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
._header
2dict
(
156 ["version", "type", "length", "id", "flags",
157 "ttl", "protocol", "checksum", "src_ip", "dst_ip"],
158 "!BBHHHBBHII", packet_data
[:20]
161 packet_size
= len(packet_data
) - 28
162 ip
= socket
.inet_ntoa(struct
.pack("!I", ip_header
["src_ip"]))
164 return receive_time
, packet_size
, ip
, ip_header
, icmp_header
166 # Check if the timeout has already been hit.
167 timeout
= timeout
- select_duration
169 return None, 0, 0, 0, 0
171 def _header2dict(self
, names
, struct_format
, data
):
173 Unpack tghe raw received IP and ICMP header informations to a dict
175 unpacked_data
= struct
.unpack(struct_format
, data
)
176 return dict(list(zip(names
, unpacked_data
)))
178 def _calculate_checksum(self
, source_string
):
179 if len(source_string
) % 2:
180 source_string
+= "\x00"
182 converted
= array
.array("H", source_string
)
183 if sys
.byteorder
== "big":
188 # Truncate val to 32 bits (a variance from ping.c, which uses signed
189 # integers, but overflow is unlinkely in ping).
192 # Add high 16 bits to low 16 bits.
193 val
= (val
>> 16) + (val
& 0xffff)
195 # Add carry from above (if any).
198 # Invert and truncate to 16 bits.
199 answer
= ~val
& 0xffff
201 return socket
.htons(answer
)
203 def _resolve(self
, host
):
207 if self
._is
_valid
_ipv
4_address
(host
):
211 return socket
.gethostbyname(host
)
212 except socket
.gaierror
as e
:
214 raise PingResolveError
218 def _is_valid_ipv4_address(self
, addr
):
220 Check addr to be a valid IPv4 address.
222 parts
= addr
.split(".")
224 if not len(parts
) == 4:
239 def receive_count(self
):
241 The number of received packets.
243 return len(self
.times
)
246 def total_time(self
):
248 The total time of all roundtrips.
251 return sum(self
.times
)
258 The smallest roundtrip time.
261 return min(self
.times
)
268 The biggest roundtrip time.
271 return max(self
.times
)
278 Calculate the average response time.
281 return self
.total_time
/ self
.receive_count
282 except ZeroDivisionError:
288 Calculate the variance of all roundtrips.
290 if self
.avg_time
is None:
296 var
+= (t
- self
.avg_time
) ** 2
298 var
/= self
.receive_count
304 Standard deviation of all roundtrips.
306 return math
.sqrt(self
.variance
)
311 Outputs the percentage of dropped packets.
313 dropped
= self
.send_count
- self
.receive_count
315 return dropped
/ self
.send_count
318 if __name__
== "__main__":
319 p
= Ping("ping.ipfire.org")
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
))