Ping? Pong!
Thursday, February 10, 2011
A few months ago, I implemented the “ping” utility in Factor. Doug Coleman helped get it working on Windows. Below I’m going to describe how it works.
First, we needed to add ICMP support in Factor (in the io.sockets.icmp
vocabulary). Some things this required:
- Previously, Factor had
inet4
andinet6
types representing a “host/port” tuple in IPv4 and IPv6, respectively. ICMP is similar in that it requires a “host”, but the “port” is unnecessary. With Slava’s help, we factored outipv4
andipv6
types frominet4
andinet6
and used them to create theicmp4
andicmp6
address types. - We needed a generic word
protocol
that can be used to specify the correct protocol value to be used when creating a socket. - We also needed an implementation of the internet
checksum,
which I had previously provided (in the
checksums.internet
vocabulary).
The ping
implementation starts with imports and a namespace:
USING: accessors byte-arrays calendar checksums
checksums.internet combinators combinators.smart continuations
destructors io.sockets io.sockets.icmp io.timeouts kernel
locals pack random sequences system ;
IN: ping
In RFC 792, the ICMP “Echo” and
“Echo Reply” packets are described. Both have the same form (except that
“Echo” is type 8 and “Echo Reply” is type 0). We define an echo
tuple
to represent both types. The <echo>
constructor is then used to create
an “Echo” (type 8) with a random identifier (some ping implementations
use the process id instead).
TUPLE: echo type identifier sequence data ;
: <echo> ( sequence data -- echo )
[ 8 16 random-bits ] 2dip echo boa ;
Since both echo types have the same form, so we can use the packet
description in the RFC to create words to convert between echo
’s and
byte-array
’s. We use the internet checksum when encoding and before
decoding to verify the response.
: echo>byte-array ( echo -- byte-array )
[
[
[ type>> 0 0 ] ! code checksum
[ identifier>> ]
[ sequence>> ] tri
] output>array "CCSSS" pack-be
] [ data>> ] bi append [
internet checksum-bytes 2 4
] keep replace-slice ;
: byte-array>echo ( byte-array -- echo )
dup internet checksum-bytes B{ 0 0 } assert=
8 cut [
"CCSSS" unpack-be { 0 3 4 } swap nths first3
] dip echo boa ;
Sending a ping is just creating an echo
request, encoding it into a
byte-array
and sending it to the specified address.
: send-ping ( addr raw -- )
[ 0 { } <echo> echo>byte-array ] 2dip send ;
Note: We should be incrementing the sequence properly, instead of always sending zero here – and then using it to verify the reply packets.
Receiving a ping is just reading packets until we have one from the specified address.
:: recv-ping ( addr raw -- echo )
raw receive addr = [
20 tail byte-array>echo
] [
drop addr raw recv-ping
] if ;
Note: There’s a subtle bug where we set a “read timeout” on the socket, but if we keep getting packets from the wrong IP, then we will loop without timing out properly.
Normally ICMP can only be used with “raw” sockets which require root
(administrative) privileges to create. This is often implemented by
setting the setuid flag on the
ping
executable. However, on BSD systems (like Mac OS), it is a
little different. Running “man 4
icmp”
shows you something called “Non-privileged ICMP” which allows you to
create an ICMP socket using the “datagram” socket (e.g., SOCK_DGRAM
).
These sockets only support a limited subset of ICMP, but it is
sufficient for sending echo requests and receiving echo replies.
HOOK: <ping-port> os ( inet -- port )
M: object <ping-port> <raw> ;
M: macosx <ping-port> <datagram> ;
Putting this all together, we can implement a ping
word that looks up
an IPv4 address for the specified hostname, and then sends a ping, using
io.timeouts
to wait up to one second for the response.
: ping ( host -- reply )
<icmp> resolve-host [ icmp4? ] filter random
f <icmp4> <ping-port>
1 seconds over set-timeout
[ [ send-ping ] [ recv-ping ] 2bi ] with-disposal ;
For convenience, we make a word to ping the IPv4 localhost address.
: local-ping ( -- reply )
"127.0.0.1" ping ;
And a word that just checks to see if a host is alive, returning t
(true) or f
(false).
: alive? ( host -- ? )
[ ping drop t ] [ 2drop f ] recover ;
This code is in extra/ping
in the Factor repository. Currently, it
supports only IPv4 addresses, but could be modified to support IPv6
(using RFC 2463 which requires
some modifications to use a “pseudo-header” in the checksum
calculation).