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 X), 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).
It seems that Windows Vista doesn't allow this unless you run the Factor listener as Administrator.
ReplyDeleteDoesn't work even if the user launching Factor has admin privileges.