* 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.
#include <netinet/ip_icmp.h>
#include "iniparser/iniparser.h"
#define MAXPACKETSIZE (65536 - 60 - 8) /* TODO: What are the magic numbers? */
#define DEFAULTDATALEN (64 - 8) /* TODO: What are the magic numbers? */
/* 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
/* Must be larger than the length of the longest configuration key (currently 'start_condition'). */
/* 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 monitor_host
* next
;
/* Since the program is based around signals, a linked list of hosts is maintained here. */
static struct monitor_host
* hosts
= NULL
;
static int send_delay
= 1;
/* Set by command line flags. */
static bool verbose
= false;
static 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(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
);
* Subtracts two timeval structs.
* Modifies out = out - in.
tv_sub(register struct timeval
* out
, register struct timeval
* in
)
if ((out
->tv_usec
-= in
->tv_usec
) < 0) {
out
->tv_sec
-= in
->tv_sec
;
* Compose and transmit an ICMP ECHO REQUEST packet. The IP packet
* will be added on by the kernel. The ID field is our UNIX process ID,
* and the sequence number is an ascending integer. The first 8 bytes
* of the data portion are used to hold a UNIX "timeval" struct in VAX
* byte-order, to compute the round-trip time.
struct monitor_host
* p
= hosts
;
unsigned char outpack
[MAXPACKETSIZE
]; /* Use char so this can be aliased later. */
gettimeofday(&now
, (struct timezone
*) NULL
);
tv_sub(&now
, &p
->last_ping_received
);
if (now
.tv_sec
> (p
->max_delay
+ p
->ping_interval
)) {
if ((p
->host_up
) || retry_down_cmd
) {
if (verbose
) printf("INFO: Host %s stopped responding. Executing DOWN command.\n", p
->name
);
gettimeofday(&now
, (struct timezone
*) NULL
);
tv_sub(&now
, &p
->last_ping_sent
);
if (now
.tv_sec
> p
->ping_interval
) { /* Time to send ping */
icp
= (struct icmp
*) outpack
;
icp
->icmp_type
= ICMP_ECHO
;
icp
->icmp_seq
= p
->socket
;
icp
->icmp_id
= getpid() & 0xFFFF;
if (verbose
) printf("INFO: Sending ICMP packet to %s.\n", p
->name
);
gettimeofday((struct timeval
*) &outpack
[8], (struct timezone
*) NULL
);
int cc
= DEFAULTDATALEN
+ 8; /* skips ICMP portion */
icp
->icmp_cksum
= checksum((uint16_t *) outpack
);
i
= sendto(p
->socket
, (char *) outpack
, cc
, 0, (const struct sockaddr
*) (&p
->dest
), sizeof(struct sockaddr
));
gettimeofday(&p
->last_ping_sent
, (struct timezone
*) NULL
);
if (i
<0) fprintf(stderr
, "WARN: Failed sending ICMP packet to %s.\n", p
->name
);
signal(SIGALRM
, pinger
); /* restore handler */
read_icmp_data(struct monitor_host
* p
)
unsigned char buf
[MAXPACKETSIZE
];
gettimeofday(&tv
, (struct timezone
*) NULL
);
if ((cc
= recvfrom(p
->socket
, buf
, sizeof(buf
), 0, (struct sockaddr
*)&from
, &fromlen
)) < 0) {
if (errno
!= EINTR
) fprintf(stderr
, "WARN: Error reading ICMP data from %s.\n", p
->name
);
/* check IP header actual len */
iphdrlen
= ip
->ip_hl
<< 2;
icmp
= (struct icmp
*) (buf
+ iphdrlen
);
if (cc
< iphdrlen
+ ICMP_MINLEN
) {
fprintf(stderr
, "WARN: Received short packet from %s.\n", p
->name
);
if (icmp
->icmp_type
== ICMP_ECHOREPLY
&& icmp
->icmp_id
== (getpid() & 0xFFFF) && icmp
->icmp_seq
== p
->socket
) {
memcpy(&p
->last_ping_received
, &tv
, sizeof(tv
));
tv_sub(&tv
, (struct timeval
*) &icmp
->icmp_data
[0]);
delay
= tv
.tv_sec
* 1000 + (tv
.tv_usec
/ 1000);
if (verbose
) printf("INFO: Got ICMP reply from %s in %d ms.\n", p
->name
, delay
);
if (verbose
) printf("INFO: Host %s started responding. Executing UP command.\n", p
->name
);
/* TODO: Do anything here? */
if (p
->socket
> maxd
) maxd
= p
->socket
;
FD_SET(p
->socket
, &rfds
);
retval
= select(maxd
+1, &rfds
, NULL
, NULL
, NULL
);
/* Intentionally empty. We arrive here when interrupted by a signal. No action should be taken. */
if (p
->socket
!=-1 && FD_ISSET(p
->socket
, &rfds
)) read_icmp_data(p
);
/* TODO: How to handle this error? */
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 monitor_host
* 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 + MAXCONFKEYLEN
+ 1);
strcpy(key_buf
, iniparser_getsecname(conf
, i
));
key_buf
[section_len
++] = ':';
struct monitor_host
* cur_host
= malloc(sizeof(struct monitor_host
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "host", MAXCONFKEYLEN
);
cur_host
->name
= strdup(iniparser_getstring(conf
, key_buf
, NULL
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "interval", MAXCONFKEYLEN
);
cur_host
->ping_interval
= iniparser_getint(conf
, key_buf
, -1);
key_buf
[section_len
] = '\0';
strncat(key_buf
, "max_delay", MAXCONFKEYLEN
);
cur_host
->max_delay
= iniparser_getint(conf
, key_buf
, -1);
key_buf
[section_len
] = '\0';
strncat(key_buf
, "up_cmd", MAXCONFKEYLEN
);
cur_host
->up_cmd
= strdup(iniparser_getstring(conf
, key_buf
, NULL
));
key_buf
[section_len
] = '\0';
strncat(key_buf
, "down_cmd", MAXCONFKEYLEN
);
cur_host
->down_cmd
= strdup(iniparser_getstring(conf
, key_buf
, NULL
));
key_buf
[section_len
] = '\0';
/* TODO: Parse for up/down/auto in start_condition. */
/* TODO: Do a host up/down check if necessary. */
cur_host
->host_up
= true;
/* TODO: Do I want to make any checks for start_condition? */
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
);
host_list_end
= cur_host
;
host_list_end
->next
= cur_host
;
host_list_end
= cur_host
;
iniparser_freedict(conf
);
get_host_addr(const char * name
)
struct hostent
* host_ent
;
if ((res
= inet_addr(name
)) < 0) {
host_ent
= gethostbyname(name
);
if (!host_ent
) return -1;
memcpy(&res
, host_ent
->h_addr
, host_ent
->h_length
);
if (remainder
== 0) return y
;
return gcd(y
, remainder
);
struct monitor_host
* p
= hosts
;
if ((proto
= getprotobyname("icmp")) == NULL
) {
fprintf(stderr
, "ERROR: Unknown protocol: icmp.\n");
bzero(&p
->dest
, sizeof(p
->dest
));
p
->dest
.sin_family
= AF_INET
;
if ((p
->dest
.sin_addr
.s_addr
= get_host_addr(p
->name
)) <= 0) {
fprintf(stderr
, "WARN: Can't resolve host. Skipping client %s.\n", p
->name
);
if ((p
->socket
= socket(AF_INET
,SOCK_RAW
,proto
->p_proto
)) < 0) {
fprintf(stderr
, "WARN: Can't create socket. Skipping client %s.\n", p
->name
);
if (ok
== 0) send_delay
= p
->ping_interval
;
else send_delay
= gcd(send_delay
, p
->ping_interval
);
fprintf(stderr
, "ERROR: No hosts left to process.\n");
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) {
fprintf(stderr
, "ERROR: Unable to parse a config file.\n");
main(int argc
, char ** argv
)
parse_params(argc
, argv
);