* Monitors hosts using ICMP 'echo', executing a user-specified command
* whenever hosts change state between responsive and unresponsive.
* © 2019 Aaron Taylor <ataylor at subgeniuskitty dot com>
* © 1999 Vadim Zaliva <lord@crocodile.org>
* © 1989 The Regents of the University of California & Mike Muuss
* See LICENSE file for copyright and license details.
/* TODO: Add IPv6 support. */
/* TODO: Turn the global '-r' functionality into per-host config file option. */
/* TODO: Add 'auto' keyword to 'start_condition', testing host on startup. */
/* TODO: Double-check the network code when interrupted while receiving a packet. */
#include <netinet/ip_icmp.h>
#include "iniparser/iniparser.h"
/* ICMP header contains: type, code, checksum, identifier and sequence number. */
#define ICMP_ECHO_HEADER_BYTES 8
#define ICMP_ECHO_DATA_BYTES sizeof(struct timeval)
#define ICMP_ECHO_PACKET_BYTES ICMP_ECHO_HEADER_BYTES + ICMP_ECHO_DATA_BYTES
#define IP_PACKET_MAX_BYTES 65535
/* Minimum time in seconds between pings. If this value is increased above the */
/* `ping_interval` for a given host, some pings to that host may not be sent. */
#define TIMER_RESOLUTION 1
/* Must be larger than the length of the longest configuration key (not value). */
/* For example: MAX_CONF_KEY_LEN > strlen("start_condition") */
#define MAX_CONF_KEY_LEN 20
/* One struct per host as listed in the config file. */
/* From the config file */
struct timeval last_ping_received
;
struct timeval last_ping_sent
;
struct host_entry
* next
;
/* Since the program is based around signals, a linked list of hosts is maintained here. */
struct host_entry
* first_host_in_list
= NULL
;
/* Set by command line flags. */
bool retry_down_cmd
= false;
* Generate an Internet Checksum per RFC 1071.
* This is not a general purpose implementation of RFC 1071. Since we only
* send ICMP echo packets, we assume 'data' will contain a specific number of
checksum(const uint16_t * data
)
uint32_t accumulator
= 0;
for (size_t i
= 0; i
< ICMP_ECHO_PACKET_BYTES
/ 2; i
++) {
accumulator
+= ntohs(data
[i
]);
if (accumulator
> 0xffff) accumulator
-= 0xffff;
return htons(~accumulator
);
* Calculate difference (a-b) between two timeval structs.
timeval_diff(struct timeval
* a
, const struct timeval
* b
)
if (a
->tv_usec
< b
->tv_usec
) {
a
->tv_usec
-= b
->tv_usec
;
* This function iterates over the list of hosts, pinging any which are due.
pinger(int ignore
) /* Dummy parameter since this function registers as a signal handler. */
assert(first_host_in_list
);
struct icmp
* icmp_packet
;
struct host_entry
* host
= first_host_in_list
;
unsigned char packet
[IP_PACKET_MAX_BYTES
]; /* Use char so this can be aliased later. */
struct timeval elapsed_time
;
gettimeofday(&elapsed_time
, NULL
);
timeval_diff(&elapsed_time
, &host
->last_ping_received
);
if ((elapsed_time
.tv_sec
> host
->max_delay
) && (host
->host_up
|| retry_down_cmd
)) {
if (verbose
) printf("INFO: Host %s stopped responding. Executing DOWN command.\n", host
->name
);
int sys_ret
= system(host
->down_cmd
);
if (elapsed_time
.tv_sec
> host
->ping_interval
) {
if (verbose
) printf("INFO: Sending ICMP packet to %s.\n", host
->name
);
icmp_packet
= (struct icmp
*) packet
;
icmp_packet
->icmp_type
= ICMP_ECHO
;
icmp_packet
->icmp_code
= 0;
icmp_packet
->icmp_cksum
= 0;
icmp_packet
->icmp_seq
= host
->socket
;
icmp_packet
->icmp_id
= getpid() & 0xFFFF;
/* Write a timestamp struct in the packet's data segment for use in calculating travel times. */
gettimeofday((struct timeval
*) &packet
[ICMP_ECHO_HEADER_BYTES
], NULL
);
icmp_packet
->icmp_cksum
= checksum((uint16_t *) packet
);
size_t bytes_sent
= sendto(host
->socket
, packet
, ICMP_ECHO_PACKET_BYTES
, 0,
(const struct sockaddr
*) &host
->dest
,
sizeof(struct sockaddr
));
if (bytes_sent
== ICMP_ECHO_PACKET_BYTES
) {
gettimeofday(&host
->last_ping_sent
, NULL
);
fprintf(stderr
, "WARN: Failed sending ICMP packet to %s.\n", host
->name
);
read_icmp_data(struct host_entry
* host
)
gettimeofday(&now
, NULL
);
socklen_t fromlen
= sizeof(from
);
unsigned char packet
[IP_PACKET_MAX_BYTES
]; /* Use char so this can be aliased later. */
if ((bytes
= recvfrom(host
->socket
, packet
, sizeof(packet
), 0, (struct sockaddr
*) &from
, &fromlen
)) < 0) {
if (errno
!= EINTR
) fprintf(stderr
, "WARN: Error reading ICMP data from %s.\n", host
->name
);
struct ip
* ip
= (struct ip
*) packet
;
int iphdrlen
= ip
->ip_hl
<< 2;
struct icmp
* icmp
= (struct icmp
*) (packet
+ iphdrlen
);
if (bytes
< iphdrlen
+ ICMP_MINLEN
) {
fprintf(stderr
, "WARN: Received short packet from %s.\n", host
->name
);
if (icmp
->icmp_type
== ICMP_ECHOREPLY
&& icmp
->icmp_id
== (getpid() & 0xFFFF) && icmp
->icmp_seq
== host
->socket
) {
memcpy(&host
->last_ping_received
, &now
, sizeof(now
));
if (verbose
) printf("INFO: Got ICMP reply from %s.\n", host
->name
);
if (verbose
) printf("INFO: Host %s started responding. Executing UP command.\n", host
->name
);
int sys_ret
= system(host
->up_cmd
);
/* The packet isn't what we expected. Ignore it and move on. */
* This function contains the main program loop, listening for replies to pings
* sent from the signal-driven pinger().
assert(first_host_in_list
);
struct host_entry
* host
= first_host_in_list
;
if (host
->socket
> max_fd
) max_fd
= host
->socket
;
FD_SET(host
->socket
, &rfds
);
if ((retval
= select(max_fd
+1, &rfds
, NULL
, NULL
, NULL
)) > 0) {
assert(first_host_in_list
);
host
= first_host_in_list
;
if (FD_ISSET(host
->socket
, &rfds
)) read_icmp_data(host
);
/* An error or interruption occurred. */
/* We can't do anything about it, so loop and try again. */
* Parse a configuration file using the `iniparser` library.
* See `icmpmonitor.ini` and `README.md` for examples and reference.
parse_config(const char * conf_file
)
dictionary
* conf
= iniparser_load(conf_file
);
fprintf(stderr
, "ERROR: Unable to parse configuration file %s.\n", conf_file
);
int host_count
= iniparser_getnsec(conf
);
fprintf(stderr
, "ERROR: Unable to determine number of hosts in configuration file.\n");
struct host_entry
* host_list_end
= NULL
;
for (int i
=0; i
< host_count
; i
++) {
/* Allocate a reusable buffer large enough to hold the full 'section:key' string. */
int section_len
= strlen(iniparser_getsecname(conf
, i
));
char * key_buf
= malloc(section_len
+ 1 + MAX_CONF_KEY_LEN
+ 1); /* +1 for ':' and '\0' */
strcpy(key_buf
, iniparser_getsecname(conf
, i
));
key_buf
[section_len
++] = ':';
struct host_entry
* cur_host
= malloc(sizeof(struct host_entry
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "host", MAX_CONF_KEY_LEN
);
cur_host
->name
= strdup(iniparser_getstring(conf
, key_buf
, NULL
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "interval", MAX_CONF_KEY_LEN
);
cur_host
->ping_interval
= iniparser_getint(conf
, key_buf
, -1);
key_buf
[section_len
] = '\0';
strncat(key_buf
, "max_delay", MAX_CONF_KEY_LEN
);
cur_host
->max_delay
= iniparser_getint(conf
, key_buf
, -1);
key_buf
[section_len
] = '\0';
strncat(key_buf
, "up_cmd", MAX_CONF_KEY_LEN
);
cur_host
->up_cmd
= strdup(iniparser_getstring(conf
, key_buf
, NULL
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "down_cmd", MAX_CONF_KEY_LEN
);
cur_host
->down_cmd
= strdup(iniparser_getstring(conf
, key_buf
, NULL
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "start_condition", MAX_CONF_KEY_LEN
);
const char * value
= iniparser_getstring(conf
, key_buf
, NULL
);
if (value
) cur_host
->host_up
= *value
== 'u' ? true : false;
if (cur_host
->name
== NULL
|| cur_host
->ping_interval
== -1 || cur_host
->max_delay
== -1) {
fprintf(stderr
, "ERROR: Problems parsing section %s.\n", iniparser_getsecname(conf
, i
));
gettimeofday(&(cur_host
->last_ping_received
), (struct timezone
*) NULL
);
if (first_host_in_list
== NULL
) {
first_host_in_list
= cur_host
;
host_list_end
= cur_host
;
host_list_end
->next
= cur_host
;
host_list_end
= cur_host
;
iniparser_freedict(conf
);
* Parse string (IP or hostname) to Internet address.
* Returns 0 if host can't be resolved, otherwise returns an Internet address.
get_host_addr(const char * name
)
memset(&hints
, 0, sizeof(hints
));
hints
.ai_family
= AF_INET
;
struct addrinfo
* address
;
if ((rv
= getaddrinfo(name
, NULL
, &hints
, &address
)) != 0) return 0;
uint32_t result
= ((struct sockaddr_in
*)(address
->ai_addr
))->sin_addr
.s_addr
;
remove_host_from_list(struct host_entry
* host
)
assert(first_host_in_list
);
if (host
== first_host_in_list
) {
first_host_in_list
= host
->next
;
struct host_entry
* temp
= first_host_in_list
;
while (temp
->next
!= host
&& temp
->next
!= NULL
) temp
= temp
->next
;
if (temp
->next
== NULL
) return;
temp
->next
= temp
->next
->next
;
struct host_entry
* host
;
if ((proto
= getprotobyname("icmp")) == NULL
) {
fprintf(stderr
, "ERROR: Unknown protocol: icmp.\n");
assert(first_host_in_list
);
host
= first_host_in_list
;
struct host_entry
* next_host
= host
->next
;
bzero(&host
->dest
, sizeof(host
->dest
));
host
->dest
.sin_family
= AF_INET
;
if (!(host
->dest
.sin_addr
.s_addr
= get_host_addr(host
->name
))) {
fprintf(stderr
, "WARN: Removing unresolvable host %s from list.\n", host
->name
);
remove_host_from_list(host
);
assert(first_host_in_list
);
host
= first_host_in_list
;
struct host_entry
* next_host
= host
->next
;
if ((host
->socket
= socket(AF_INET
, SOCK_RAW
, proto
->p_proto
)) < 0) {
fprintf(stderr
, "WARN: Failed creating socket. Removing host %s from list.\n", host
->name
);
remove_host_from_list(host
);
print_usage(char ** argv
)
printf( "ICMPmonitor v%d (www.subgeniuskitty.com)\n"
"Usage: %s [-h] [-v] [-r] -f <file>\n"
" -v Verbose mode. Prints message for each packet sent and received.\n"
" -r Repeat down_cmd every time a host fails to respond to a packet.\n"
" Note: Default behavior executes down_cmd only once, resetting once the host is back up.\n"
" -h Help (prints this message)\n"
" -f <file> Specify a configuration file.\n"
parse_params(int argc
, char ** argv
)
while ((param
= getopt(argc
, argv
, "hrvf:")) != -1) {
if (first_host_in_list
== NULL
) {
fprintf(stderr
, "ERROR: Unable to parse a config file.\n");
main(int argc
, char ** argv
)
/* Parse the command line options, load and parse the config file. */
parse_params(argc
, argv
);
/* Process config for each host, generating/verifying any necessary information. */
/* Make sure initialization left us with something useful. */
assert(first_host_in_list
);
/* Pings are sent asynchronously. */
/* The main program loop listens for ping responses. */
/* Should be unreachable. */