/* ex: set tabstop=4 shiftwidth=4 autoindent: +-------------------------------------------------------------------------+ | Copyright (C) 2004-2026 The Cacti Group | | | | This program is free software; you can redistribute it and/or | | modify it under the terms of the GNU Lesser General Public | | License as published by the Free Software Foundation; either | | version 2.1 of the License, or (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Lesser General Public License for more details. | | | | You should have received a copy of the GNU Lesser General Public | | License along with this library; if not, write to the Free Software | | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | | 02110-1301, USA | | | +-------------------------------------------------------------------------+ | spine: a backend data gatherer for cacti | +-------------------------------------------------------------------------+ | This poller would not have been possible without: | | - Larry Adams (current development and enhancements) | | - Rivo Nurges (rrd support, mysql poller cache, misc functions) | | - RTG (core poller code, pthreads, snmp, autoconf examples) | | - Brady Alleman/Doug Warner (threading ideas, implementation details) | +-------------------------------------------------------------------------+ | - Cacti - http://www.cacti.net/ | +-------------------------------------------------------------------------+ */ #include "common.h" #include "spine.h" #include #ifdef SPINE_HAVE_ICMPV6 static int ping_icmp_ipv6(const host_t *host, ping_t *ping); #endif static const char *ping_address(const char *hostname, char *address, size_t capacity); /*! \fn int ping_host(host_t *host, ping_t *ping) * \brief ping a host to determine if it is reachable for polling * \param host a pointer to the current host structure * \param ping a pointer to the current hosts ping structure * * This function pings a host using the method specified within the system * configuration and then returns the host status to the calling function. * * \return HOST_UP if the host is reachable, HOST_DOWN otherwise. */ static int ping_network(host_t *host, ping_t *ping) { if (host->availability.ping_method == PING_ICMP && !set.availability.icmp_avail) { SPINE_LOG(("Device[%i] DEBUG Falling back to UDP Ping Due to SetUID Issues", host->id)); host->availability.ping_method = PING_UDP; } if (strstr(host->hostname, "localhost")) { STRNCOPY(ping->ping_status, "0.000"); STRNCOPY(ping->ping_response, "PING: Device does not require ping."); return HOST_UP; } int address_type = get_address_type(host); #ifdef SPINE_HAVE_ICMPV6 if (address_type == SPINE_IPV6 && host->availability.ping_method == PING_ICMP) { return ping_icmp_ipv6(host, ping); } #endif if (address_type != SPINE_IPV4) { if (host->availability.method == AVAIL_PING) { STRNCOPY(ping->ping_status, "0.000"); STRNCOPY(ping->ping_response, "PING: Device is Unknown or is IPV6. Please use the SNMP ping options only."); } return HOST_DOWN; } switch (host->availability.ping_method) { case PING_ICMP: return ping_icmp(host, ping); case PING_UDP: return ping_udp(host, ping); case PING_TCP: case PING_TCP_CLOSED: return ping_tcp(host, ping); default: return HOST_DOWN; } } static int ping_snmp_availability(host_t *host, ping_t *ping, int ping_result) { if (host->availability.method == AVAIL_SNMP_AND_PING && ping_result != HOST_UP) return HOST_DOWN; if (host->availability.method == AVAIL_SNMP_OR_PING && ping_result == HOST_UP) return HOST_UP; /* Preserve the configured no-SNMP contract for v1/v2 without a community. */ if (host->snmp.profile.community[0] == '\0' && host->snmp.profile.version < 3) return HOST_UP; double begin = get_time_as_double(); int result = ping_snmp(host, ping); double elapsed = (get_time_as_double() - begin) * 1000.0; SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] INFO: SNMP Device %s, Time:%.4f ms", host->id, result == HOST_UP ? "Alive" : "Down", elapsed)); return result; } int ping_host(host_t *host, ping_t *ping) { int network_result = HOST_DOWN; if (host->availability.method == AVAIL_SNMP_AND_PING || host->availability.method == AVAIL_PING || host->availability.method == AVAIL_SNMP_OR_PING) { network_result = ping_network(host, ping); } switch (host->availability.method) { case AVAIL_SNMP_AND_PING: case AVAIL_SNMP_OR_PING: case AVAIL_SNMP: case AVAIL_SNMP_GET_NEXT: case AVAIL_SNMP_GET_SYSDESC: return ping_snmp_availability(host, ping, network_result); case AVAIL_PING: return network_result; case AVAIL_NONE: return HOST_UP; default: return HOST_DOWN; } } /*! \fn int ping_snmp(host_t *host, ping_t *ping) * \brief ping a host using snmp sysUptime * \param host a pointer to the current host structure * \param ping a pointer to the current hosts ping structure * * This function pings a host using snmp. It polls sysUptime by default. * It will modify the ping structure to include the specifics of the ping results. * * \return HOST_UP if the host is reachable, HOST_DOWN otherwise. * */ int ping_snmp(host_t *host, ping_t *ping) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_DEBUG, ("Device[%i] DEBUG: Entering SNMP Ping", host->id)); if (!host->snmp.session) { STRNCOPY(ping->snmp_status, "0.00"); STRNCOPY(ping->snmp_response, "Invalid SNMP Session"); return HOST_DOWN; } if (host->snmp.profile.community[0] == '\0' && host->snmp.profile.version != 3) { STRNCOPY(ping->snmp_status, "0.00"); STRNCOPY(ping->snmp_response, "Device does not require SNMP"); return HOST_UP; } char oid[32]; switch (host->availability.method) { case AVAIL_SNMP_GET_NEXT: STRNCOPY(oid, ".1.3"); break; case AVAIL_SNMP_GET_SYSDESC: STRNCOPY(oid, ".1.3.6.1.2.1.1.1.0"); break; default: STRNCOPY(oid, ".1.3.6.1.2.1.1.3.0"); break; } double begin = get_time_as_double(); char *result = host->availability.method == AVAIL_SNMP_GET_NEXT ? snmp_getnext(host, oid) : snmp_get(host, oid); double elapsed = (get_time_as_double() - begin) * 1000.0; SPINE_FREE(result); if (host->snmp.status == SNMPERR_SUCCESS || host->snmp.status == SNMPERR_UNKNOWN_OBJID) { STRNCOPY(ping->snmp_response, "Device responded to SNMP"); spine_snprintf(ping->snmp_status, sizeof(ping->snmp_status), "%.5f", elapsed); return HOST_UP; } SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] SNMP Ping %s", host->id, host->snmp.status == STAT_TIMEOUT ? "Timeout" : "Unknown Error")); STRNCOPY(ping->snmp_response, "Device did not respond to SNMP"); return HOST_DOWN; } static int ping_down(ping_t *ping, const char *message); /*! \fn static void ping_wait_left(double deadline, struct timeval *timeout) * \brief converts the time left before a monotonic deadline to a timeval * * Each ICMP attempt owns a deadline of its own. Measuring from the first * attempt left every retry with an already spent budget, so one lost packet * marked the device down, and rint(ms / 1000) plus the millisecond remainder * stretched a 1500 ms timeout to 2.5 s. */ static void ping_wait_left(double deadline, struct timeval *timeout) { double remaining = deadline - spine_monotonic_time(); if (remaining < 0) { remaining = 0; } timeout->tv_sec = (time_t) remaining; timeout->tv_usec = (suseconds_t) ((remaining - (double) timeout->tv_sec) * 1000000); } #ifdef __CYGWIN__ /*! \fn static void icmp_discard_reply(int icmp_socket, char *buffer) * \brief consumes a datagram that ping_icmp() has only peeked at * * Cygwin cannot use MSG_WAITALL on a raw socket, so ping_icmp() reads with * MSG_PEEK. A peeked datagram stays queued, so a reply we decline has to be * drained here. Otherwise the next pass reads the same bytes again and the * loop spins until the device timeout expires. */ static void icmp_discard_reply(int icmp_socket, char *buffer) { while (recvfrom(icmp_socket, buffer, BUFSIZE, 0, NULL, NULL) < 0 && errno == EINTR) { /* interrupted before the datagram was removed, try again */ } } #define ICMP_DISCARD_PEEKED(sock, buf) icmp_discard_reply((sock), (buf)) #else #define ICMP_DISCARD_PEEKED(sock, buf) ((void)0) #endif /*! \fn spine_icmp_reply_t spine_icmp_classify_reply(const unsigned char *reply, ssize_t len, uint16_t want_id, uint16_t want_seq, const struct icmp **out_pkt) * \brief bounds-check a raw ICMP reply and decide whether it answers our echo * * The raw socket is shared across every poller thread and is fed by whatever * the network sends, so this walks the IP header length field before touching * the ICMP header. Split out of ping_icmp() so the fuzz target exercises the * same code the poller runs. * * \return the reply classification; *out_pkt is set only for SPINE_ICMP_REPLY_OK */ spine_icmp_reply_t spine_icmp_classify_reply(const unsigned char *reply, ssize_t len, uint16_t want_id, uint16_t want_seq, const struct icmp **out_pkt) { size_t ihl; uint16_t received_id; uint16_t received_seq; if (out_pkt != NULL) { *out_pkt = NULL; } if (reply == NULL || len < (ssize_t) sizeof(struct ip)) { return SPINE_ICMP_REPLY_TOO_SHORT; } /* Read header fields as bytes: the receive buffer has no alignment * guarantee, and a raw socket can deliver a non-IPv4 or non-ICMP datagram. */ ihl = (size_t) (reply[0] & 0x0F) << 2; if ((reply[0] >> 4) != 4 || reply[9] != IPPROTO_ICMP || ihl < sizeof(struct ip) || len < (ssize_t) (ihl + ICMP_HDR_SIZE)) { return SPINE_ICMP_REPLY_BAD_HEADER; } /* An echo reply always carries code 0. */ if (reply[ihl] != ICMP_ECHOREPLY || reply[ihl + 1] != 0) { return SPINE_ICMP_REPLY_NOT_ECHO; } memcpy(&received_id, reply + ihl + 4, sizeof(received_id)); memcpy(&received_seq, reply + ihl + 6, sizeof(received_seq)); if (received_id != want_id || received_seq != want_seq) { return SPINE_ICMP_REPLY_NOT_OURS; } if (out_pkt != NULL) { *out_pkt = (const struct icmp *) (reply + ihl); } return SPINE_ICMP_REPLY_OK; } /*! \fn spine_icmp_reply_t spine_icmp_classify_dgram_reply(const unsigned char *reply, ssize_t len, uint16_t want_seq, const struct icmp **out_pkt) * \brief classifies an echo reply read from an unprivileged datagram socket * * A SOCK_DGRAM ICMP socket delivers the reply with the IPv4 header already * stripped, and the kernel assigns the echo id rather than honouring the one * we wrote, so the sequence is all that is left to match on. The caller has * already checked the source address. */ spine_icmp_reply_t spine_icmp_classify_dgram_reply(const unsigned char *reply, ssize_t len, uint16_t want_seq, const struct icmp **out_pkt) { const struct icmp *pkt; uint16_t received_seq; if (out_pkt != NULL) { *out_pkt = NULL; } if (reply == NULL || len < (ssize_t) ICMP_HDR_SIZE) { return SPINE_ICMP_REPLY_TOO_SHORT; } pkt = (const struct icmp *) reply; /* Fields are read as bytes; the receive buffer has no alignment guarantee. */ if (reply[0] != ICMP_ECHOREPLY || reply[1] != 0) { return SPINE_ICMP_REPLY_NOT_ECHO; } memcpy(&received_seq, reply + 6, sizeof(received_seq)); if (received_seq != want_seq) { return SPINE_ICMP_REPLY_NOT_OURS; } if (out_pkt != NULL) { *out_pkt = pkt; } return SPINE_ICMP_REPLY_OK; } #ifdef SPINE_HAVE_ICMPV6 static const char icmp6_payload[] = "cacti-monitoring-system"; /*! \fn static int icmp6_reply_matches(const unsigned char *reply, ssize_t len, uint16_t id, uint16_t seq, int check_id) * \brief decides whether an ICMPv6 message answers our echo request * * ICMPv6 sockets deliver the message without the IPv6 header. id and seq * are in network order, as written into the request. A datagram socket * rewrites the echo id, so its caller passes check_id FALSE. */ static int icmp6_reply_matches(const unsigned char *reply, ssize_t len, uint16_t id, uint16_t seq, int check_id) { struct icmp6_hdr hdr; size_t payload_len = sizeof(icmp6_payload) - 1; if (len < 0 || (size_t) len < sizeof(hdr) + payload_len) { return FALSE; } memcpy(&hdr, reply, sizeof(hdr)); if (hdr.icmp6_type != ICMP6_ECHO_REPLY || hdr.icmp6_seq != seq) { return FALSE; } if (check_id && hdr.icmp6_id != id) { return FALSE; } return memcmp(reply + sizeof(hdr), icmp6_payload, payload_len) == 0; } #endif /* Raw sockets opened by drop_privileges() before a setuid root spine gave up * root for good. Without CAP_NET_RAW nothing can open another, so every * poller thread shares them. A raw socket queues each ICMP message once, so a * thread that reads a reply meant for another thread must hand it over rather * than drop it: one thread at a time reads, matches each reply against the * registered requests, and wakes only the thread it answers. LOCK_ICMP guards * all of it. */ typedef struct { int fd; int reading; } icmp_shared_t; typedef struct icmp_waiter { struct icmp_waiter *next; icmp_shared_t *shared; pthread_cond_t wake; int family; uint16_t id; uint16_t seq; struct in_addr peer; #ifdef SPINE_HAVE_ICMPV6 struct in6_addr peer6; #endif int answered; int sleeping; } icmp_waiter_t; /* A reader empties the queue in batches read without LOCK_ICMP held, so a * flood delays neither registrations nor the waiters being woken. The bound * on one pass keeps the reader's own deadline check running. Matching needs * only the IP header, the ICMP header and the payload marker, so each slot * keeps the first ICMP_SHARED_SNAPLEN bytes. The queue itself is sized for * every poller thread rather than one. */ #define ICMP_SHARED_DRAIN_MAX 1024 #define ICMP_SHARED_BATCH 64 #define ICMP_SHARED_SNAPLEN 128 #define ICMP_SHARED_RCVBUF (4 * 1024 * 1024) #if defined(__linux__) #ifndef SOL_RAW #define SOL_RAW 255 #endif #ifndef ICMP_FILTER #define ICMP_FILTER 1 #endif #endif static icmp_shared_t icmp_shared = {-1, FALSE}; #ifdef SPINE_HAVE_ICMPV6 static icmp_shared_t icmp6_shared = {-1, FALSE}; #endif static icmp_waiter_t *icmp_waiters = NULL; /*! \fn static int icmp_open_socket(int family, int type, int protocol) * \brief opens an ICMP socket that PHP and script children cannot inherit * * Poller threads posix_spawn() scripts while others ping, so the flag is set * atomically where SOCK_CLOEXEC exists; elsewhere fcntl() narrows the window. */ static int icmp_open_socket(int family, int type, int protocol) { int fd; #ifdef SOCK_CLOEXEC fd = socket(family, type | SOCK_CLOEXEC, protocol); if (fd != -1 || errno != EINVAL) { return fd; } #endif fd = socket(family, type, protocol); if (fd != -1 && fcntl(fd, F_SETFD, FD_CLOEXEC) == -1) { close(fd); return -1; } return fd; } static int icmp_open_shared(int family, int protocol) { int fd = icmp_open_socket(family, SOCK_RAW, protocol); if (fd == -1) { return -1; } /* A descriptor select() cannot watch is no use to the readers below. */ if (fd >= FD_SETSIZE) { close(fd); return -1; } #if defined(__linux__) /* The kernel copies every inbound ICMP message to a raw socket. Echo * requests and errors aimed at this host would otherwise compete with * the replies for the single shared queue. Best effort, as is * ICMP6_FILTER on the IPv6 socket. */ if (family == AF_INET) { uint32_t blocked = ~(1U << ICMP_ECHOREPLY); (void) setsockopt(fd, SOL_RAW, ICMP_FILTER, &blocked, sizeof(blocked)); } #endif /* Best effort as well. SO_RCVBUFFORCE passes net.core.rmem_max, which * is why this happens here, while spine is still root. */ { int rcvbuf = ICMP_SHARED_RCVBUF; #ifdef SO_RCVBUFFORCE if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf)) != 0) #endif (void) setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); } return fd; } /*! \fn int ping_icmp_open_shared(void) * \brief opens the shared raw ICMP sockets; the caller must still be root * * \return TRUE if the IPv4 socket is open. IPv6 is best effort. */ int ping_icmp_open_shared(void) { if (icmp_shared.fd == -1) { icmp_shared.fd = icmp_open_shared(AF_INET, IPPROTO_ICMP); } #ifdef SPINE_HAVE_ICMPV6 if (icmp6_shared.fd == -1) { icmp6_shared.fd = icmp_open_shared(AF_INET6, IPPROTO_ICMPV6); } #endif return icmp_shared.fd != -1; } int ping_icmp_shared_available(void) { return icmp_shared.fd != -1; } /* Called with LOCK_ICMP held whenever nobody is reading a socket: wake one * sleeping waiter so it takes over. A waiter that is not asleep finds the * socket free on its own before it next sleeps. */ static void icmp_shared_handoff(const icmp_shared_t *shared) { icmp_waiter_t *waiter; if (shared->reading) { return; } for (waiter = icmp_waiters; waiter != NULL; waiter = waiter->next) { if (waiter->shared == shared && waiter->sleeping && !waiter->answered) { pthread_cond_signal(&waiter->wake); return; } } } static void icmp_shared_register(icmp_waiter_t *waiter) { pthread_cond_init(&waiter->wake, NULL); thread_mutex_lock(LOCK_ICMP); waiter->answered = FALSE; waiter->sleeping = FALSE; waiter->next = icmp_waiters; icmp_waiters = waiter; thread_mutex_unlock(LOCK_ICMP); } static void icmp_shared_unregister(icmp_waiter_t *waiter) { icmp_waiter_t **link; thread_mutex_lock(LOCK_ICMP); for (link = &icmp_waiters; *link != NULL; link = &(*link)->next) { if (*link == waiter) { *link = waiter->next; break; } } icmp_shared_handoff(waiter->shared); thread_mutex_unlock(LOCK_ICMP); pthread_cond_destroy(&waiter->wake); } /* Called with LOCK_ICMP held. A reply nobody waits for is dropped, exactly as * a per-thread socket would drop a reply that is not its own. */ static void icmp_shared_dispatch(int family, const unsigned char *reply, ssize_t len, const struct sockaddr_storage *from) { icmp_waiter_t *waiter; for (waiter = icmp_waiters; waiter != NULL; waiter = waiter->next) { if (waiter->family != family || waiter->answered) { continue; } if (family == AF_INET) { const struct sockaddr_in *source = (const struct sockaddr_in *) from; if (source->sin_addr.s_addr != waiter->peer.s_addr || spine_icmp_classify_reply(reply, len, waiter->id, waiter->seq, NULL) != SPINE_ICMP_REPLY_OK) { continue; } } #ifdef SPINE_HAVE_ICMPV6 else { const struct sockaddr_in6 *source = (const struct sockaddr_in6 *) from; if (memcmp(&source->sin6_addr, &waiter->peer6, sizeof(waiter->peer6)) != 0 || !icmp6_reply_matches(reply, len, waiter->id, waiter->seq, TRUE)) { continue; } } #endif waiter->answered = TRUE; pthread_cond_signal(&waiter->wake); return; } } /*! \fn static int icmp_shared_await(icmp_waiter_t *waiter, double deadline) * \brief waits until the reply to a registered request arrives, or deadline * * Whichever waiting thread finds the socket idle reads it, so a reply is * never stuck behind a thread whose own deadline is further away: a reader * returns from select() as soon as anything arrives, drains the queue, and * wakes the threads whose replies it found. * * \return TRUE if the reply arrived in time. */ static int icmp_shared_await(icmp_waiter_t *waiter, double deadline) { icmp_shared_t *shared = waiter->shared; unsigned char replies[ICMP_SHARED_BATCH][ICMP_SHARED_SNAPLEN]; struct sockaddr_storage sources[ICMP_SHARED_BATCH]; ssize_t lengths[ICMP_SHARED_BATCH]; socklen_t fromlen; ssize_t received; struct timeval wait; struct timespec until; fd_set fds; double now; double remaining; int readable; int drained; int count; int slot; int answered; until.tv_sec = (time_t) deadline; until.tv_nsec = (long) ((deadline - (double) until.tv_sec) * 1000000000.0); thread_mutex_lock(LOCK_ICMP); while (!waiter->answered) { now = get_time_as_double(); if (now >= deadline) { break; } if (shared->reading) { waiter->sleeping = TRUE; pthread_cond_timedwait(&waiter->wake, get_lock(LOCK_ICMP), &until); waiter->sleeping = FALSE; continue; } shared->reading = TRUE; thread_mutex_unlock(LOCK_ICMP); remaining = deadline - now; wait.tv_sec = (time_t) remaining; wait.tv_usec = (suseconds_t) ((remaining - (double) wait.tv_sec) * 1000000.0); FD_ZERO(&fds); FD_SET(shared->fd, &fds); readable = select(shared->fd + 1, &fds, NULL, NULL, &wait) > 0; /* Empty the queue: under a flood, one reply per pass falls behind * and the kernel drops the replies that matter. */ for (drained = 0; readable && drained < ICMP_SHARED_DRAIN_MAX; drained += count) { for (count = 0; count < ICMP_SHARED_BATCH; ) { fromlen = sizeof(sources[count]); received = recvfrom(shared->fd, replies[count], ICMP_SHARED_SNAPLEN, MSG_DONTWAIT, (struct sockaddr *) &sources[count], &fromlen); if (received < 0) { if (errno == EINTR) { continue; } readable = FALSE; break; } lengths[count++] = received; } thread_mutex_lock(LOCK_ICMP); for (slot = 0; slot < count; slot++) { if (lengths[slot] > 0) { icmp_shared_dispatch(waiter->family, replies[slot], lengths[slot], &sources[slot]); } } thread_mutex_unlock(LOCK_ICMP); } thread_mutex_lock(LOCK_ICMP); shared->reading = FALSE; } answered = waiter->answered; /* Leaving with the socket idle: let a sleeping waiter take over. */ icmp_shared_handoff(shared); thread_mutex_unlock(LOCK_ICMP); return answered; } /*! \fn int ping_icmp(host_t *host, ping_t *ping) * \brief ping a host using an ICMP packet * \param host a pointer to the current host structure * \param ping a pointer to the current hosts ping structure * * This function pings a host using ICMP. The ICMP packet contains a marker * to the "Cacti" application so that firewall's can be configured to allow. * It will modify the ping structure to include the specifics of the ping results. * * \return HOST_UP if the host is reachable, HOST_DOWN otherwise. * */ int ping_icmp(const host_t *host, ping_t *ping) { int icmp_socket = -1; int icmp_dgram; int rc = HOST_DOWN; int socket_errno = 0; int icmp_use_shared = FALSE; icmp_waiter_t waiter; int waiting = FALSE; double begin_time, deadline, total_time; double host_timeout; double one_thousand = 1000.00; struct timeval timeout; struct sockaddr_in recvname; struct sockaddr_in fromname; char socket_reply[BUFSIZE]; int retry_count; const char *cacti_msg = "cacti-monitoring-system\0"; int packet_len; socklen_t fromlen; ssize_t return_code; fd_set socket_fds; static unsigned int seq = 0; struct icmp *icmp; unsigned char *packet = NULL; if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Entering ICMP Ping", host->id)); } else { SPINE_LOG_DEBUG(("Device[%i] DEBUG: Entering ICMP Ping", host->id)); } /* Linux hands out SOCK_DGRAM ICMP sockets to the groups listed in * net.ipv4.ping_group_range, so try that before asking for root. When * the sysctl excludes us the call fails and the raw path below runs * unchanged. A datagram reply arrives with the IPv4 header already * stripped, which the receive path has to account for. */ icmp_dgram = FALSE; icmp_socket = icmp_open_socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP); if (icmp_socket != -1) { icmp_dgram = TRUE; } else if (icmp_shared.fd != -1) { icmp_socket = icmp_shared.fd; icmp_use_shared = TRUE; } /* Otherwise open a raw socket of our own, which needs CAP_NET_RAW or a * real root user. Spine never regains root to do it: a setuid install * without capabilities uses the shared socket above instead. */ retry_count = 0; while (icmp_socket == -1) { icmp_socket = icmp_open_socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); socket_errno = errno; if (icmp_socket != -1) { break; } SPINE_LOG_MEDIUM(("WARNING: Device[%i] raw ICMP socket creation failed: %s", host->id, strerror(socket_errno))); retry_count++; if (retry_count > 4) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Ping unable to create ICMP Socket"); snprintf(ping->ping_status, 50, "down"); rc = HOST_DOWN; goto cleanup; } usleep(500000); } /* convert the host timeout to a double precision number in seconds */ host_timeout = host->availability.timeout; /* allocate the packet in memory */ packet_len = ICMP_HDR_SIZE + strlen(cacti_msg); if (!(packet = malloc(packet_len))) { die("ERROR: Fatal malloc error: ping.c ping_icmp!"); } memset(packet, 0, packet_len); /* set the memory of the ping address */ memset(&fromname, 0, sizeof(struct sockaddr_in)); memset(&recvname, 0, sizeof(struct sockaddr_in)); icmp = (struct icmp*) packet; icmp->icmp_type = ICMP_ECHO; icmp->icmp_code = 0; icmp->icmp_id = getpid() & 0xFFFF; /* lock set/get the sequence and unlock */ thread_mutex_lock(LOCK_GHBN); icmp->icmp_seq = seq++; thread_mutex_unlock(LOCK_GHBN); icmp->icmp_cksum = 0; memcpy(packet+ICMP_HDR_SIZE, cacti_msg, strlen(cacti_msg)); icmp->icmp_cksum = get_checksum(packet, packet_len); /* hostname must be nonblank */ if ((strlen(host->hostname) != 0) && (icmp_socket != -1)) { /* initialize variables */ snprintf(ping->ping_status, 50, "down"); snprintf(ping->ping_response, SMALL_BUFSIZE, "default"); /* get address of hostname */ if (init_sockaddr(&fromname, host->hostname, 7)) { retry_count = 0; total_time = 0; if (icmp_use_shared) { memset(&waiter, 0, sizeof(waiter)); waiter.shared = &icmp_shared; waiter.family = AF_INET; waiter.id = (uint16_t) (getpid() & 0xFFFF); waiter.seq = icmp->icmp_seq; waiter.peer = fromname.sin_addr; icmp_shared_register(&waiter); waiting = TRUE; } while (1) { if (retry_count > host->availability.retries) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Ping timed out"); snprintf(ping->ping_status, 50, "down"); rc = HOST_DOWN; goto cleanup; } if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Attempting to ping %s, seq %d (Retry %d of %d)", host->id, host->hostname, icmp->icmp_seq, retry_count, host->availability.retries)); } else { SPINE_LOG_DEBUG(("Device[%i] DEBUG: Attempting to ping %s, seq %d (Retry %d of %d)", host->id, host->hostname, icmp->icmp_seq, retry_count, host->availability.retries)); } if (waiting) { /* Socket options would change every other thread's socket too. */ double attempt_begin = get_time_as_double(); if (sendto(icmp_socket, packet, packet_len, 0, (struct sockaddr *) &fromname, sizeof(fromname)) >= 0 && icmp_shared_await(&waiter, attempt_begin + host_timeout / one_thousand)) { total_time = (get_time_as_double() - attempt_begin) * one_thousand; if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] INFO: ICMP Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } else { SPINE_LOG_MEDIUM(("Device[%i] INFO: ICMP Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Device is Alive"); snprintf(ping->ping_status, 50, "%.5f", total_time); rc = HOST_UP; goto cleanup; } retry_count++; continue; } begin_time = spine_monotonic_time(); deadline = begin_time + host_timeout / one_thousand; ping_wait_left(deadline, &timeout); /* set the socket send and receive timeout */ setsockopt(icmp_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); setsockopt(icmp_socket, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); /* send packet to destination */ return_code = sendto(icmp_socket, packet, packet_len, 0, (struct sockaddr *) &fromname, sizeof(fromname)); fromlen = sizeof(fromname); /* wait for a response on the socket */ /* reinitialize fd_set -- select(2) clears bits in place on return */ keep_listening: FD_ZERO(&socket_fds); if (icmp_socket >= FD_SETSIZE) { SPINE_LOG(("ERROR: Device[%i] ICMP socket %d exceeds FD_SETSIZE %d", host->id, icmp_socket, FD_SETSIZE)); snprintf(ping->ping_status, 50, "down"); snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: fd exceeds FD_SETSIZE"); rc = HOST_DOWN; goto cleanup; } FD_SET(icmp_socket,&socket_fds); /* a stray datagram must not restart the wait, and only Linux * writes the time left back into the timeval */ ping_wait_left(deadline, &timeout); return_code = select(icmp_socket + 1, &socket_fds, NULL, NULL, &timeout); if (return_code < 0 && errno == EINTR) { goto keep_listening; } total_time = (spine_monotonic_time() - begin_time) * one_thousand; if (return_code > 0 && total_time < host_timeout) { #if !(defined(__CYGWIN__)) return_code = recvfrom(icmp_socket, socket_reply, BUFSIZE, MSG_WAITALL, (struct sockaddr *) &recvname, &fromlen); #else return_code = recvfrom(icmp_socket, socket_reply, BUFSIZE, MSG_PEEK, (struct sockaddr *) &recvname, &fromlen); #endif if (return_code < 0) { if (errno == EINTR) { /* call was interrupted by some system event */ if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Received EINTR", host->id)); } else { SPINE_LOG_DEBUG(("Device[%i] DEBUG: Received EINTR", host->id)); } goto keep_listening; } } else { const struct icmp *reply_pkt = NULL; const struct icmp *seen = NULL; spine_icmp_reply_t verdict; if (icmp_dgram) { verdict = spine_icmp_classify_dgram_reply((const unsigned char *) socket_reply, return_code, icmp->icmp_seq, &reply_pkt); } else { verdict = spine_icmp_classify_reply((const unsigned char *) socket_reply, return_code, (uint16_t)(getpid() & 0xFFFF), icmp->icmp_seq, &reply_pkt); } if (verdict == SPINE_ICMP_REPLY_NOT_OURS) { /* a middlebox that rewrites ICMP query ids is indistinguishable * from a dead device once the reply is dropped, so record what * arrived. Both classifiers validate the length before they can * return this, so the header is safe to read here */ if (icmp_dgram) { seen = (const struct icmp *) socket_reply; } else { seen = (const struct icmp *) (socket_reply + (((struct ip *) socket_reply)->ip_hl << 2)); } SPINE_LOG_DEBUG(("DEBUG: Device[%i] Discarded ICMP reply, expected id:%d seq:%d, received id:%d seq:%d", host->id, (int)(getpid() & 0xFFFF), icmp->icmp_seq, seen->icmp_id, seen->icmp_seq)); } if (verdict != SPINE_ICMP_REPLY_OK && verdict != SPINE_ICMP_REPLY_NOT_ECHO) { ICMP_DISCARD_PEEKED(icmp_socket, socket_reply); goto keep_listening; } if (fromname.sin_addr.s_addr == recvname.sin_addr.s_addr) { if (verdict == SPINE_ICMP_REPLY_OK) { if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] INFO: ICMP Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } else { SPINE_LOG_MEDIUM(("Device[%i] INFO: ICMP Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Device is Alive"); snprintf(ping->ping_status, 50, "%.5f", total_time); rc = HOST_UP; goto cleanup; } else { /* received a response other than an echo reply; the * echo reply may still arrive inside this deadline */ ICMP_DISCARD_PEEKED(icmp_socket, socket_reply); goto keep_listening; } } else { /* another host responded */ ICMP_DISCARD_PEEKED(icmp_socket, socket_reply); goto keep_listening; } } } else { if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Exceeded Device Timeout, Retrying", host->id)); } else { SPINE_LOG_DEBUG(("Device[%i] DEBUG: Exceeded Device Timeout, Retrying", host->id)); } } total_time = 0; retry_count++; #ifndef SOLAR_THREAD usleep(1000); #endif } } else { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Destination hostname invalid"); snprintf(ping->ping_status, 50, "down"); rc = HOST_DOWN; goto cleanup; } } else { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMP: Destination address not specified"); snprintf(ping->ping_status, 50, "down"); rc = HOST_DOWN; goto cleanup; } cleanup: /* One owner for both resources. Five exits used to spell this out * separately and they had already drifted: the FD_SETSIZE guard closed the * socket and returned without freeing the packet, once per affected device * per cycle. See #593. * * The shared socket outlives this call; only a socket opened here is * closed. */ SPINE_FREE(packet); if (waiting) { icmp_shared_unregister(&waiter); } if (icmp_socket != -1 && !icmp_use_shared) { close(icmp_socket); } return rc; } #ifdef SPINE_HAVE_ICMPV6 /*! \fn static int init_sockaddr6(struct sockaddr_in6 *name, const char *hostname) * \brief converts a hostname or IPv6 literal to an IPv6 socket address * * \return TRUE if successful, FALSE otherwise. * */ static int init_sockaddr6(struct sockaddr_in6 *name, const char *hostname) { struct addrinfo hints, *hostinfo; int rv, retry_count; char address[BUFSIZE]; memset(&hints, 0, sizeof(hints)); /* AI_ADDRCONFIG is deliberately omitted: it hides IPv6 results on hosts * that only carry a loopback address, which is exactly where a ::1 test * has to work */ hints.ai_family = AF_INET6; hints.ai_socktype = SOCK_DGRAM; retry_count = 0; hostinfo = NULL; while (TRUE) { rv = getaddrinfo(ping_address(hostname, address, sizeof(address)), NULL, &hints, &hostinfo); if (rv == 0) { break; } if ((rv == EAI_AGAIN) && (retry_count < 3)) { retry_count++; usleep(50000); continue; } SPINE_LOG(("WARNING: Error resolving IPv6 host %s (%s)", hostname, gai_strerror(rv))); return FALSE; } if (hostinfo == NULL) { SPINE_LOG(("WARNING: Unknown host %s", hostname)); return FALSE; } if ((hostinfo->ai_family != AF_INET6) || (hostinfo->ai_addrlen < sizeof(struct sockaddr_in6))) { SPINE_LOG(("WARNING: Host %s did not resolve to an IPv6 address", hostname)); freeaddrinfo(hostinfo); return FALSE; } memcpy(name, hostinfo->ai_addr, sizeof(struct sockaddr_in6)); /* on a raw IPv6 socket sin6_port carries the protocol number rather * than a port, and the kernel rejects anything that disagrees with the * socket's own protocol */ name->sin6_port = 0; freeaddrinfo(hostinfo); return TRUE; } /*! \fn static void apply_ipv6_scope_id(struct sockaddr_in6 *name) * \brief supplies a scope id for a link local destination that lacks one * * getaddrinfo() only fills in sin6_scope_id when the literal carries a * %zone suffix. Without it the kernel refuses to send, so fall back to * the first non loopback interface that carries an IPv6 address. * */ static void apply_ipv6_scope_id(struct sockaddr_in6 *name) { #if defined(HAVE_IFADDRS_H) && defined(HAVE_NET_IF_H) && defined(HAVE_GETIFADDRS) && defined(HAVE_IF_NAMETOINDEX) struct ifaddrs *ifa_list, *ifa; unsigned int if_index; ifa_list = NULL; if (getifaddrs(&ifa_list) != 0) { return; } for (ifa = ifa_list; ifa != NULL; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL) continue; if (ifa->ifa_addr->sa_family != AF_INET6) continue; if (ifa->ifa_flags & IFF_LOOPBACK) continue; if_index = if_nametoindex(ifa->ifa_name); if (if_index != 0) { name->sin6_scope_id = if_index; break; } } freeifaddrs(ifa_list); #else (void) name; #endif } /*! \fn static int ping_icmp_ipv6(host_t *host, ping_t *ping) * \brief ping a host using an ICMPv6 echo request * \param host a pointer to the current host structure * \param ping a pointer to the current hosts ping structure * * The IPv6 counterpart of ping_icmp(). The payload carries the same * "Cacti" marker so that firewalls can be configured to allow it, and the * kernel computes the ICMPv6 checksum on our behalf. It will modify the * ping structure to include the specifics of the ping results. * * \return HOST_UP if the host is reachable, HOST_DOWN otherwise. * */ static int ping_icmp_ipv6(const host_t *host, ping_t *ping) { int icmp_socket; int socket_errno = 0; int icmp_use_shared = FALSE; icmp_waiter_t waiter; int waiting = FALSE; double begin_time, deadline, total_time; double host_timeout; double one_thousand = 1000.00; struct timeval timeout; struct sockaddr_in6 recvname; struct sockaddr_in6 fromname; char socket_reply[BUFSIZE]; int retry_count; const char *cacti_msg = icmp6_payload; size_t msg_len; int packet_len; socklen_t fromlen; ssize_t return_code; fd_set socket_fds; int result; static unsigned int seq = 0; struct icmp6_hdr *icmp6; unsigned char *packet; uint16_t our_id; uint16_t our_seq; int icmp_dgram; if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Entering ICMPv6 Ping", host->id)); } else { SPINE_LOG_DEBUG(("DEBUG: Device[%i] Entering ICMPv6 Ping", host->id)); } result = HOST_DOWN; packet = NULL; icmp_socket = -1; retry_count = 0; msg_len = strlen(cacti_msg); /* net.ipv4.ping_group_range governs IPPROTO_ICMPV6 datagram sockets as * well, so the unprivileged path is worth trying here too. The kernel * rewrites the echo id on such a socket, which the reply match below * accounts for. */ icmp_dgram = FALSE; icmp_socket = icmp_open_socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6); if (icmp_socket != -1) { icmp_dgram = TRUE; } else if (icmp6_shared.fd != -1) { icmp_socket = icmp6_shared.fd; icmp_use_shared = TRUE; } /* As in ping_icmp(), a socket of our own never involves regaining root. */ while (icmp_socket == -1) { icmp_socket = icmp_open_socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); socket_errno = errno; if (icmp_socket != -1) { break; } SPINE_LOG_MEDIUM(("WARNING: Device[%i] raw ICMPv6 socket creation failed: %s", host->id, strerror(socket_errno))); usleep(500000); retry_count++; if (retry_count > 4) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: Ping unable to create ICMPv6 Socket"); snprintf(ping->ping_status, 50, "down"); return HOST_DOWN; } } /* RFC 3542 hardening. Both options are best effort because older * kernels and restricted sandboxes reject them without breaking the * ping itself */ { struct icmp6_filter filter; ICMP6_FILTER_SETBLOCKALL(&filter); ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter); if (setsockopt(icmp_socket, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) < 0) { SPINE_LOG_DEBUG(("DEBUG: ICMP6_FILTER not supported: %s", strerror(errno))); } } #ifdef IPV6_CHECKSUM { /* tell the kernel where to write the checksum it computes for us */ int cksum_offset = (int) offsetof(struct icmp6_hdr, icmp6_cksum); if (setsockopt(icmp_socket, IPPROTO_IPV6, IPV6_CHECKSUM, &cksum_offset, sizeof(cksum_offset)) < 0) { SPINE_LOG_DEBUG(("DEBUG: IPV6_CHECKSUM not supported: %s", strerror(errno))); } } #endif /* convert the host timeout to a double precision number in seconds */ host_timeout = host->availability.timeout; /* allocate the packet in memory */ packet_len = (int) (sizeof(struct icmp6_hdr) + msg_len); if (!(packet = malloc(packet_len))) { die("ERROR: Fatal malloc error: ping.c ping_icmp_ipv6!"); } memset(packet, 0, packet_len); /* set the memory of the ping address */ memset(&fromname, 0, sizeof(struct sockaddr_in6)); memset(&recvname, 0, sizeof(struct sockaddr_in6)); our_id = (uint16_t) (getpid() & 0xFFFF); /* lock set/get the sequence and unlock */ thread_mutex_lock(LOCK_GHBN); our_seq = (uint16_t) seq++; thread_mutex_unlock(LOCK_GHBN); icmp6 = (struct icmp6_hdr *) packet; icmp6->icmp6_type = ICMP6_ECHO_REQUEST; icmp6->icmp6_code = 0; icmp6->icmp6_id = htons(our_id); icmp6->icmp6_seq = htons(our_seq); icmp6->icmp6_cksum = 0; memcpy(packet + sizeof(struct icmp6_hdr), cacti_msg, msg_len); /* hostname must be nonblank */ if (strlen(host->hostname) == 0) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: Destination address not specified"); snprintf(ping->ping_status, 50, "down"); goto cleanup; } /* get address of hostname */ if (!init_sockaddr6(&fromname, host->hostname)) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: Destination hostname invalid"); snprintf(ping->ping_status, 50, "down"); goto cleanup; } if (IN6_IS_ADDR_LINKLOCAL(&fromname.sin6_addr) && (fromname.sin6_scope_id == 0)) { apply_ipv6_scope_id(&fromname); } /* initialize variables */ snprintf(ping->ping_status, 50, "down"); snprintf(ping->ping_response, SMALL_BUFSIZE, "default"); retry_count = 0; total_time = 0; if (icmp_use_shared) { memset(&waiter, 0, sizeof(waiter)); waiter.shared = &icmp6_shared; waiter.family = AF_INET6; waiter.id = htons(our_id); waiter.seq = htons(our_seq); waiter.peer6 = fromname.sin6_addr; icmp_shared_register(&waiter); waiting = TRUE; } while (1) { if (retry_count > host->availability.retries) { snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: Ping timed out"); snprintf(ping->ping_status, 50, "down"); goto cleanup; } if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Attempting to ping %s, seq %d (Retry %d of %d)", host->id, host->hostname, our_seq, retry_count, host->availability.retries)); } else { SPINE_LOG_DEBUG(("DEBUG: Device[%i] Attempting to ping %s, seq %d (Retry %d of %d)", host->id, host->hostname, our_seq, retry_count, host->availability.retries)); } if (waiting) { /* Socket options would change every other thread's socket too. */ double attempt_begin = get_time_as_double(); if (sendto(icmp_socket, packet, packet_len, 0, (struct sockaddr *) &fromname, sizeof(fromname)) >= 0 && icmp_shared_await(&waiter, attempt_begin + host_timeout / one_thousand)) { total_time = (get_time_as_double() - attempt_begin) * one_thousand; if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] INFO: ICMPv6 Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } else { SPINE_LOG_MEDIUM(("Device[%i] INFO: ICMPv6 Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: Device is Alive"); snprintf(ping->ping_status, 50, "%.5f", total_time); result = HOST_UP; goto cleanup; } retry_count++; continue; } begin_time = spine_monotonic_time(); deadline = begin_time + host_timeout / one_thousand; ping_wait_left(deadline, &timeout); /* set the socket send and receive timeout */ setsockopt(icmp_socket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); setsockopt(icmp_socket, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout)); /* send packet to destination */ if (sendto(icmp_socket, packet, packet_len, 0, (struct sockaddr *) &fromname, sizeof(fromname)) < 0) { SPINE_LOG_DEBUG(("DEBUG: Device[%i] ICMPv6 sendto failed (%s)", host->id, strerror(errno))); total_time = 0; retry_count++; continue; } /* wait for a response on the socket */ /* reinitialize fd_set -- select(2) clears bits in place on return */ keep_listening_ipv6: FD_ZERO(&socket_fds); if (icmp_socket >= FD_SETSIZE) { SPINE_LOG(("ERROR: Device[%i] ICMPv6 socket %d exceeds FD_SETSIZE %d", host->id, icmp_socket, FD_SETSIZE)); snprintf(ping->ping_status, 50, "down"); snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: fd exceeds FD_SETSIZE"); goto cleanup; } FD_SET(icmp_socket,&socket_fds); /* a stray datagram must not restart the wait, and only Linux * writes the time left back into the timeval */ ping_wait_left(deadline, &timeout); return_code = select(icmp_socket + 1, &socket_fds, NULL, NULL, &timeout); if (return_code < 0 && errno == EINTR) { goto keep_listening_ipv6; } total_time = (spine_monotonic_time() - begin_time) * one_thousand; if ((return_code > 0) && (total_time < host_timeout)) { fromlen = sizeof(recvname); return_code = recvfrom(icmp_socket, socket_reply, BUFSIZE, 0, (struct sockaddr *) &recvname, &fromlen); if (return_code < 0) { if (errno == EINTR) { /* call was interrupted by some system event */ if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Received EINTR", host->id)); } else { SPINE_LOG_DEBUG(("DEBUG: Device[%i] Received EINTR", host->id)); } goto keep_listening_ipv6; } } else { /* the kernel does not match the source address for us */ if (memcmp(fromname.sin6_addr.s6_addr, recvname.sin6_addr.s6_addr, sizeof(fromname.sin6_addr.s6_addr)) != 0) { /* another host responded */ goto keep_listening_ipv6; } /* on a datagram socket the kernel assigns the echo id, so it * will not match what we wrote; sequence, source address and * payload still identify the reply */ if (!icmp6_reply_matches((const unsigned char *) socket_reply, return_code, htons(our_id), htons(our_seq), !icmp_dgram)) { goto keep_listening_ipv6; } if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] INFO: ICMPv6 Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } else { SPINE_LOG_MEDIUM(("Device[%i] INFO: ICMPv6 Device Alive, Try Count:%i, Time:%.4f ms", host->id, retry_count+1, (total_time))); } snprintf(ping->ping_response, SMALL_BUFSIZE, "ICMPv6: Device is Alive"); snprintf(ping->ping_status, 50, "%.5f", total_time); result = HOST_UP; goto cleanup; } } else { if (is_debug_device(host->id)) { SPINE_LOG(("Device[%i] DEBUG: Exceeded Device Timeout, Retrying", host->id)); } else { SPINE_LOG_DEBUG(("DEBUG: Device[%i] Exceeded Device Timeout, Retrying", host->id)); } } total_time = 0; retry_count++; #ifndef SOLAR_THREAD usleep(1000); #endif } cleanup: SPINE_FREE(packet); if (waiting) { icmp_shared_unregister(&waiter); } if (icmp_socket != -1 && !icmp_use_shared) { close(icmp_socket); } return result; } #endif /* SPINE_HAVE_ICMPV6 */ /*! \fn int ping_udp(const host_t *host, ping_t *ping) * \brief ping a host using an UDP datagram * \param host a pointer to the current host structure * \param ping a pointer to the current hosts ping structure * * This function pings a host using UDP. The UDP datagram contains a marker * to the "Cacti" application so that firewall's can be configured to allow. * It will modify the ping structure to include the specifics of the ping results. * * \return HOST_UP if the host is reachable, HOST_DOWN otherwise. * */ static int ping_down(ping_t *ping, const char *message) { snprintf(ping->ping_status, 50, "down"); strncopy(ping->ping_response, message, SMALL_BUFSIZE); return HOST_DOWN; } /* A connected socket accepts datagrams only from its peer. Port-unreachable * errors also indicate a response, but EHOSTUNREACH cannot distinguish a * firewall rejection from an unreachable host or failed neighbour lookup. It * is neither proof of life nor a reason to stop retrying, so the wait * continues until the deadline and the caller decides whether to try again. */ static int ping_udp_response(int fd, double deadline) { char response[BUFSIZE]; for (;;) { int ready = spine_wait_readable(fd, deadline); if (ready <= 0) return ready; ssize_t received = recv(fd, response, sizeof(response), 0); if (received >= 0) return 1; if (errno == ECONNRESET || errno == ECONNREFUSED) return 1; if (errno == EHOSTUNREACH || errno == ENETUNREACH) continue; #ifdef EHOSTDOWN if (errno == EHOSTDOWN) continue; #endif if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) return -1; } } int ping_udp(const host_t *host, ping_t *ping) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_DEBUG, ("Device[%i] DEBUG: Entering UDP Ping", host->id)); if (host->hostname[0] == '\0') return ping_down(ping, "UDP: Destination address invalid or unable to create socket"); struct sockaddr_in servername = {0}; if (!init_sockaddr(&servername, host->hostname, host->availability.port)) return ping_down(ping, "UDP: Destination hostname invalid"); if (host->availability.timeout <= 0 || host->availability.retries < 0) return ping_down(ping, "UDP: Ping timed out"); int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (fd < 0) return ping_down(ping, "UDP: Destination address invalid or unable to create socket"); int flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0 || connect(fd, (struct sockaddr *)&servername, sizeof(servername)) < 0) { close(fd); return ping_down(ping, "UDP: Cannot connect to host"); } static const char request[] = "cacti-monitoring-system"; double begin = spine_monotonic_time(); for (unsigned int attempt = 0; ; attempt++) { double deadline = spine_monotonic_time() + (double)host->availability.timeout / 1000; ssize_t sent; do { sent = send(fd, request, sizeof(request) - 1, 0); } while (sent < 0 && errno == EINTR && spine_monotonic_time() < deadline); int result = sent == (ssize_t)(sizeof(request) - 1) ? ping_udp_response(fd, deadline) : -1; double elapsed = (spine_monotonic_time() - begin) * 1000; if (result > 0) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] INFO: UDP Device Alive, Try Count:%u, Time:%.4f ms", host->id, attempt + 1, elapsed)); strncopy(ping->ping_response, "UDP: Device is Alive", SMALL_BUFSIZE); snprintf(ping->ping_status, 50, "%.5f", elapsed); close(fd); return HOST_UP; } if (result < 0 || attempt >= (unsigned int)host->availability.retries) { close(fd); return ping_down(ping, result < 0 ? "UDP: Device is Down" : "UDP: Ping timed out"); } SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_DEBUG, ("Device[%i] DEBUG: UDP Timeout, Try Count:%u, Time:%.4f ms", host->id, attempt + 1, elapsed)); } } /*! \fn int ping_tcp(const host_t *host, ping_t *ping) * \brief ping a host using an TCP syn * \param host a pointer to the current host structure * \param ping a pointer to the current hosts ping structure * * This function pings a host using TCP. The TCP socket contains a marker * to the "Cacti" application so that firewall's can be configured to allow. * It will modify the ping structure to include the specifics of the ping results. * * \return HOST_UP if the host is reachable, HOST_DOWN otherwise. * */ /* Each retry owns a new nonblocking socket; a failed connect socket must not * be reused. SO_ERROR distinguishes completion from refused/timed-out peers. */ static int ping_tcp_complete(int fd, double deadline) { int ready = spine_wait_writable(fd, deadline); if (ready <= 0) return ready == 0 ? ETIMEDOUT : errno; int error = 0; socklen_t length = sizeof(error); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) < 0) return errno; return error; } static int ping_tcp_attempt(int fd, const struct sockaddr_in *address, double deadline) { int flags = fcntl(fd, F_GETFL, 0); if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return errno; if (connect(fd, (const struct sockaddr *)address, sizeof(*address)) == 0) return 0; int error = errno; if (error != EINPROGRESS && error != EINTR) return error; return ping_tcp_complete(fd, deadline); } static int ping_tcp_connect(const struct sockaddr_in *address, double deadline) { int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (fd < 0) return errno; int error = ping_tcp_attempt(fd, address, deadline); close(fd); return error; } int ping_tcp(const host_t *host, ping_t *ping) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_DEBUG, ("Device[%i] DEBUG: Entering TCP Ping", host->id)); if (host->hostname[0] == '\0') return ping_down(ping, "TCP: Destination address invalid or unable to create socket"); struct sockaddr_in address = {0}; if (!init_sockaddr(&address, host->hostname, host->availability.port)) return ping_down(ping, "TCP: Destination hostname invalid"); if (host->availability.timeout <= 0 || host->availability.retries < 0) return ping_down(ping, "TCP: Cannot connect to host"); double begin = spine_monotonic_time(); for (unsigned int attempt = 0; ; attempt++) { double deadline = spine_monotonic_time() + (double)host->availability.timeout / 1000; int error = ping_tcp_connect(&address, deadline); if (error == 0 || (error == ECONNREFUSED && host->availability.ping_method == PING_TCP_CLOSED)) { double elapsed = (spine_monotonic_time() - begin) * 1000; SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_MEDIUM, ("Device[%i] INFO: TCP Device Alive, Try Count:%u, Time:%.4f ms", host->id, attempt + 1, elapsed)); strncopy(ping->ping_response, "TCP: Device is Alive", SMALL_BUFSIZE); snprintf(ping->ping_status, 50, "%.5f", elapsed); return HOST_UP; } /* Cygwin historically makes one attempt only. */ #if defined(__CYGWIN__) return ping_down(ping, "TCP: Cannot connect to host"); #else if (attempt >= (unsigned int)host->availability.retries) return ping_down(ping, "TCP: Cannot connect to host"); #endif } } /*! \fn int get_address_type(host_t *host) * \brief determines using getaddrinfo the iptype and returns the iptype * * \return 1 - IPv4, 2 - IPv6, 0 - Unknown */ int get_address_type(host_t *host) { struct addrinfo hints; struct addrinfo *res_list; char addrstr[255]; char address[BUFSIZE]; const void *ptr = NULL; int addr_found = FALSE; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_flags = AI_CANONNAME | AI_ADDRCONFIG; int error; if ((error = getaddrinfo(ping_address(host->hostname, address, sizeof(address)), NULL, &hints, &res_list)) != 0) { SPINE_LOG(("WARNING: Unable to determine address info for %s (%s)", host->hostname, gai_strerror(error))); return SPINE_NONE; } for (struct addrinfo *res = res_list; res != NULL; res = res->ai_next) { inet_ntop(res->ai_family, res->ai_addr->sa_data, addrstr, 100); switch(res->ai_family) { case AF_INET: ptr = &((struct sockaddr_in *) res->ai_addr)->sin_addr; addr_found = TRUE; break; case AF_INET6: ptr = &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr; addr_found = TRUE; break; default: continue; } inet_ntop(res->ai_family, ptr, addrstr, 100); SPINE_LOG_HIGH(("Device[%d] IPv%d address %s (%s)", host->id, res->ai_family == PF_INET6 ? 6:4, addrstr, res->ai_canonname)); if (res->ai_family != PF_INET6) { freeaddrinfo(res_list); return SPINE_IPV4; } } freeaddrinfo(res_list); if (addr_found) { return SPINE_IPV6; } else { return SPINE_NONE; } } /*! \fn int init_sockaddr(struct sockaddr_in *name, const char *hostname, int port) * \brief converts a hostname to an internet address * * \return TRUE if successful, FALSE otherwise. * */ int init_sockaddr(struct sockaddr_in *name, const char *hostname, int port) { if (port < 0 || port > 65535) return FALSE; struct addrinfo hints = {0}; struct addrinfo *hostinfo = NULL; char address[BUFSIZE]; hints.ai_family = AF_INET; hints.ai_flags = AI_CANONNAME | AI_ADDRCONFIG; ping_address(hostname, address, sizeof(address)); for (int attempt = 0; ; attempt++) { int status = getaddrinfo(address, NULL, &hints, &hostinfo); if (status == 0) break; if (status == EAI_AGAIN && attempt < 3) { SPINE_LOG(("WARNING: Temporary DNS error for host %s (%s), retrying", hostname, gai_strerror(status))); spine_sleep_usec(50000); continue; } SPINE_LOG(("WARNING: Error resolving host %s (%s)", hostname, gai_strerror(status))); return FALSE; } if (hostinfo == NULL) { SPINE_LOG(("WARNING: Unknown host %s", hostname)); return FALSE; } name->sin_family = AF_INET; name->sin_addr = ((struct sockaddr_in *)hostinfo->ai_addr)->sin_addr; name->sin_port = htons((unsigned short)port); freeaddrinfo(hostinfo); return TRUE; } /*! \fn static int split_host_spec(const char *spec, int *method, char *address, size_t capacity, int *port) * \brief splits a "[transport:]host[:port]" device name into its parts * * Cacti keeps Net-SNMP transport specifiers such as udp6:[2001:db8::1]:161 * in the hostname. An IPv6 host is either bracketed, so that a port can * follow it, or a bare literal in which every colon belongs to the address. * A port that is not a plain number from 1 to 65535 is reported as 0. * * \return TRUE if the host part is IPv6, FALSE otherwise */ static int split_host_spec(const char *spec, int *method, char *address, size_t capacity, int *port) { static const char *const methods[] = {"TCP", "UDP", "TCP6", "UDP6"}; const char *host = spec; const char *colon; const char *end; const char *port_text = NULL; size_t length; size_t i; int ipv6 = FALSE; *method = 0; *port = 0; if (capacity == 0) { return FALSE; } colon = strchr(spec, ':'); if (colon != NULL) { for (i = 0; i < sizeof(methods) / sizeof(methods[0]); i++) { length = strlen(methods[i]); if ((size_t) (colon - spec) == length && strncasecmp(spec, methods[i], length) == 0) { *method = (int) i + 1; host = colon + 1; break; } } } if (host[0] == '[' && (end = strchr(host, ']')) != NULL) { ipv6 = TRUE; host = host + 1; length = (size_t) (end - host); if (end[1] == ':') { port_text = end + 2; } } else if (char_count(host, ':') > 1 || host[0] == '[') { ipv6 = TRUE; length = strlen(host); } else if ((colon = strchr(host, ':')) != NULL) { length = (size_t) (colon - host); port_text = colon + 1; } else { length = strlen(host); } if (length >= capacity) { length = capacity - 1; } snprintf(address, capacity, "%.*s", (int) length, host); if (port_text != NULL && port_text[0] >= '0' && port_text[0] <= '9') { char *rest; long value; errno = 0; value = strtol(port_text, &rest, 10); if (errno == 0 && *rest == '\0' && value > 0 && value <= 65535) { *port = (int) value; } } return ipv6; } /*! \fn static const char *ping_address(const char *hostname, char *address, size_t capacity) * \brief the bare address a network probe resolves for a device name */ static const char *ping_address(const char *hostname, char *address, size_t capacity) { int method; int port; split_host_spec(hostname, &method, address, capacity, &port); return address; } /*! \fn name_t *get_namebyhost(const char *hostname, name_t *name) * \brief parses a device hostname with an optional transport and port * * An IPv6 name is returned whole: Net-SNMP needs the transport and the * brackets to reach the agent, and the ping code strips them itself. */ name_t *get_namebyhost(const char *hostname, name_t *name) { char address[BUFSIZE]; if (name == NULL) { name = calloc(1, sizeof(*name)); if (name == NULL) die("ERROR: Fatal malloc error: ping.c get_namebyhost->name"); } if (split_host_spec(hostname, &name->method, address, sizeof(address), &name->port)) { strncopy(name->hostname, hostname, sizeof(name->hostname)); } else { strncopy(name->hostname, address, sizeof(name->hostname)); } return name; } /*! \fn unsigned short int get_checksum(void* buf, int len) * \brief calculates a 16bit checksum of a packet buffer * \param buf the input buffer to calculate the checksum of * \param len the size of the input buffer * * \return 16bit checksum of an input buffer of size len. * */ unsigned short int get_checksum(void* buf, int len) { int nleft = len; int32_t sum = 0; unsigned short int answer; unsigned short int* w = (unsigned short int*)buf; unsigned short int odd_byte = 0; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(unsigned char*)(&odd_byte) = *(unsigned char*)w; sum += odd_byte; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = (unsigned short int)((unsigned int)~sum & 0xffff); /* checksum truncation is intentional */ return answer; } static bool host_requires_snmp(const host_t *host) { return host->snmp.profile.community[0] != '\0' || host->snmp.profile.version >= 3; } static void host_failure_message(host_t *host, const ping_t *ping, int method) { switch (method) { case AVAIL_SNMP_OR_PING: case AVAIL_SNMP_AND_PING: if (host_requires_snmp(host)) { snprintf(host->state.status_last_error, BUFSIZE * 2 + 1, "%s, %s", ping->snmp_response, ping->ping_response); } else { snprintf(host->state.status_last_error, BUFSIZE * 2 + 1, "%s", ping->ping_response); } break; case AVAIL_SNMP: snprintf(host->state.status_last_error, BUFSIZE * 2 + 1, "%s", host_requires_snmp(host) ? ping->snmp_response : "Device does not require SNMP"); break; default: snprintf(host->state.status_last_error, BUFSIZE * 2 + 1, "%s", ping->ping_response); } } static bool host_failure_transition(host_t *host, const char *date) { switch (host->state.status) { case HOST_UP: host->state.status_event_count++; if (host->state.status_event_count >= set.availability.ping_failure_count) { host->state.status = HOST_DOWN; if (set.availability.ping_failure_count == 1) snprintf(host->state.status_fail_date, sizeof(host->state.status_fail_date), "%s", date); return TRUE; } if (host->state.status_event_count == 1) snprintf(host->state.status_fail_date, sizeof(host->state.status_fail_date), "%s", date); return FALSE; case HOST_RECOVERING: host->state.status_event_count = 1; host->state.status = HOST_DOWN; return FALSE; case HOST_UNKNOWN: host->state.status = HOST_DOWN; host->state.status_event_count = 0; return FALSE; default: host->state.status_event_count++; return FALSE; } } static bool host_recovery_transition(host_t *host, const char *date) { if (host->state.status != HOST_DOWN && host->state.status != HOST_RECOVERING) { host->state.status = HOST_UP; host->state.status_event_count = 0; return FALSE; } if (host->state.status == HOST_DOWN) { host->state.status = HOST_RECOVERING; host->state.status_event_count = 1; } else { host->state.status_event_count++; } if (host->state.status_event_count >= set.availability.ping_recovery_count) { host->state.status = HOST_UP; if (set.availability.ping_recovery_count == 1) snprintf(host->state.status_rec_date, sizeof(host->state.status_rec_date), "%s", date); host->state.status_event_count = 0; return TRUE; } if (host->state.status_event_count == 1) snprintf(host->state.status_rec_date, sizeof(host->state.status_rec_date), "%s", date); return FALSE; } static double host_response_time(const host_t *host, const ping_t *ping, int method) { switch (method) { case AVAIL_SNMP_AND_PING: return host_requires_snmp(host) ? (atof(ping->snmp_status) + atof(ping->ping_status)) / 2 : atof(ping->ping_status); case AVAIL_SNMP: return host_requires_snmp(host) ? atof(ping->snmp_status) : 0.0; case AVAIL_NONE: return 0.0; default: return atof(ping->ping_status); } } static void host_response_statistics(host_t *host, double ping_time) { host->statistics.cur_time = ping_time; if (ping_time > host->statistics.max_time) host->statistics.max_time = ping_time; if (ping_time < host->statistics.min_time) host->statistics.min_time = ping_time; host->statistics.avg_time = (((host->statistics.total_polls - 1 - host->statistics.failed_polls) * host->statistics.avg_time) + ping_time) / (host->statistics.total_polls - host->statistics.failed_polls); } static void log_host_availability(const host_t *host, const ping_t *ping, int method) { if (set.logging.log_level < POLLER_VERBOSITY_HIGH) return; bool up = host->state.status == HOST_UP || host->state.status == HOST_RECOVERING; if (method == AVAIL_SNMP_AND_PING || (method == AVAIL_SNMP_OR_PING && up)) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] PING Result: %s", host->id, ping->ping_response)); SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] SNMP Result: %s", host->id, ping->snmp_response)); } else if (method == AVAIL_SNMP) { const char *message = up && !host_requires_snmp(host) ? "Device does not require SNMP" : ping->snmp_response; SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] SNMP Result: %s", host->id, message)); } else if (method == AVAIL_NONE) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] No Device Availability Method Selected", host->id)); } else if (up) { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] PING: Result %s", host->id, ping->ping_response)); } else { SPINE_LOG_DEVICE(host->id, POLLER_VERBOSITY_HIGH, ("Device[%i] PING Result: %s", host->id, ping->ping_response)); } } /*! Update availability statistics and apply failure/recovery thresholds. */ void update_host_status(int status, host_t *host, const ping_t *ping, int availability_method) { char current_date[40]; snprintf(current_date, sizeof(current_date), "%lu", time(NULL)); bool issue_log_message; host->statistics.total_polls++; if (status == HOST_DOWN) { host->statistics.failed_polls++; host_failure_message(host, ping, availability_method); issue_log_message = host_failure_transition(host, current_date); } else { host_response_statistics(host, host_response_time(host, ping, availability_method)); issue_log_message = host_recovery_transition(host, current_date); } host->statistics.availability = 100.0 * (host->statistics.total_polls - host->statistics.failed_polls) / host->statistics.total_polls; log_host_availability(host, ping, availability_method); if (!issue_log_message) return; if (host->state.status == HOST_DOWN) { SPINE_LOG(("Device[%i] Hostname[%s] ERROR: HOST EVENT: Device is DOWN Message: %s", host->id, host->hostname, host->state.status_last_error)); } else { SPINE_LOG(("Device[%i] Hostname[%s] NOTICE: HOST EVENT: Device Returned from DOWN State", host->id, host->hostname)); } }