Thursday, February 10, 2011

Ping? Pong!

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 and inet6 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 out ipv4 and ipv6 types from inet4 and inet6 and used them to create the icmp4 and icmp6 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).

1 comment:

otoburb said...

It seems that Windows Vista doesn't allow this unless you run the Factor listener as Administrator.

Doesn't work even if the user launching Factor has admin privileges.