diff --git a/configure.ac b/configure.ac index 4094e35f87..0e55c5cb7f 100644 --- a/configure.ac +++ b/configure.ac @@ -186,6 +186,8 @@ AC_ARG_ENABLE(regex-timers, [AS_HELP_STRING([--enable-regex-timers], [build with HTTP_GET regex timers])]) AC_ARG_ENABLE(json, [AS_HELP_STRING([--enable-json], [compile with signal to dump configuration and stats as json])]) +AC_ARG_ENABLE(status-socket, + [AS_HELP_STRING([--enable-status-socket], [enable unix socket for status queries and health checks])]) AC_ARG_ENABLE(clang, [AS_HELP_STRING([--enable-clang], [use clang compiler])]) AC_ARG_ENABLE(lto, @@ -2372,6 +2374,7 @@ VRRP_SUPPORT=No VRRP_AUTH_SUPPORT=No MACVLAN_SUPPORT=No ENABLE_JSON=No +STATUS_SOCKET_SUPPORT=No BFD_SUPPORT=No HAVE_CN_PROC=No WITH_TRACK_PROCESS=No @@ -2436,6 +2439,13 @@ if test "$enable_vrrp" != no; then add_config_opt([JSON]) fi + dnl ----[ Status socket support ? ]---- + if test "${enable_status_socket}" = yes; then + STATUS_SOCKET_SUPPORT=Yes + AC_DEFINE([_WITH_STATUS_SOCKET_], [ 1 ], [Define to 1 to enable status socket for health checks]) + add_config_opt([STATUS_SOCKET]) + fi + dnl ----[ BFD support ? ]---- if test "${enable_bfd}" = yes; then BFD_SUPPORT=Yes @@ -2454,6 +2464,7 @@ AM_CONDITIONAL([WITH_VRRP], [test $VRRP_SUPPORT = Yes]) AM_CONDITIONAL([VRRP_AUTH], [test $VRRP_AUTH_SUPPORT = Yes]) AM_CONDITIONAL([VMAC], [test $MACVLAN_SUPPORT = Yes]) AM_CONDITIONAL([WITH_JSON], [test $ENABLE_JSON = Yes]) +AM_CONDITIONAL([WITH_STATUS_SOCKET], [test $STATUS_SOCKET_SUPPORT = Yes]) AM_CONDITIONAL([WITH_BFD], [test $BFD_SUPPORT = Yes]) AM_CONDITIONAL([TRACK_PROCESS], [test $WITH_TRACK_PROCESS = Yes]) diff --git a/doc/man/man5/keepalived.conf.5.in b/doc/man/man5/keepalived.conf.5.in index 5e968e2286..681b5fea9f 100644 --- a/doc/man/man5/keepalived.conf.5.in +++ b/doc/man/man5/keepalived.conf.5.in @@ -723,6 +723,28 @@ possibly following any cleanup actions needed. # (default: "none") \fBdbus_no_interface_name \fRNAME + # If Keepalived has been built with status socket support + # (--enable-status-socket), the following keywords are available. + # -- + # Enable a Unix domain socket for unified status queries and health checks. + # The socket runs in the parent process and aggregates state from all + # child daemons (VRRP, Checker, BFD). + # The socket accepts simple text commands: + # HEALTH - returns "MASTER", "BACKUP", or "FAULT" based on aggregated state + # FAULT if any daemon reports fault state + # MASTER if VRRP has at least one instance in MASTER state + # BACKUP otherwise + # STATUS - returns JSON with details from all running daemons: + # {"vrrp":{...},"checker":{...},"bfd":{...}} + # Useful for container health checks (e.g., Kubernetes liveness probes). + # Example: echo "HEALTH" | socat - UNIX:/var/run/keepalived/status.sock + # (default: /var/run/keepalived/status.sock) + \fBstatus_socket \fRPATH + + # Unix file permissions for the status socket + # (default: 0600) + \fBstatus_socket_mode \fRMODE + # Specify the default username/groupname to run scripts under. # If this option is not specified, the user defaults to keepalived_script # if that user exists, otherwise the uid/gid under which keepalived is running. diff --git a/keepalived/bfd/bfd_daemon.c b/keepalived/bfd/bfd_daemon.c index bc85498434..ede5cc6800 100644 --- a/keepalived/bfd/bfd_daemon.c +++ b/keepalived/bfd/bfd_daemon.c @@ -59,6 +59,9 @@ #ifndef _ONE_PROCESS_DEBUG_ #include "config_notify.h" #endif +#ifdef _WITH_STATUS_SOCKET_ +#include "status_event.h" +#endif /* Global variables */ @@ -427,6 +430,35 @@ start_bfd_child(void) close(bfd_checker_event_pipe[0]); #endif +#ifdef _WITH_STATUS_SOCKET_ + /* BFD child keeps only write end of its status pipe */ + if (status_bfd_pipe[0] >= 0) { + close(status_bfd_pipe[0]); + status_bfd_pipe[0] = -1; + } + /* Close all of VRRP and checker status pipes */ +#ifdef _WITH_VRRP_ + if (status_vrrp_pipe[0] >= 0) { + close(status_vrrp_pipe[0]); + status_vrrp_pipe[0] = -1; + } + if (status_vrrp_pipe[1] >= 0) { + close(status_vrrp_pipe[1]); + status_vrrp_pipe[1] = -1; + } +#endif +#ifdef _WITH_LVS_ + if (status_checker_pipe[0] >= 0) { + close(status_checker_pipe[0]); + status_checker_pipe[0] = -1; + } + if (status_checker_pipe[1] >= 0) { + close(status_checker_pipe[1]); + status_checker_pipe[1] = -1; + } +#endif +#endif + #ifdef THREAD_DUMP /* Remove anything we might have inherited from parent */ deregister_thread_addresses(); diff --git a/keepalived/bfd/bfd_event.c b/keepalived/bfd/bfd_event.c index 4a8e573b5f..01faed0c94 100644 --- a/keepalived/bfd/bfd_event.c +++ b/keepalived/bfd/bfd_event.c @@ -27,6 +27,7 @@ #include "bfd.h" #include "bfd_event.h" #include "bfd_daemon.h" +#include "bfd_data.h" #include "logger.h" #include "main.h" #include "memory.h" @@ -34,6 +35,37 @@ #include "utils.h" #include "global_data.h" #include "assert_debug.h" +#ifdef _WITH_STATUS_SOCKET_ +#include "status_event.h" +#include "list_head.h" + +static void +bfd_send_status_event(void) +{ + bfd_t *bfd; + uint32_t num_inst = 0; + uint32_t num_down = 0; + uint8_t state; + + if (!bfd_data || list_empty(&bfd_data->bfd)) + return; + + list_for_each_entry(bfd, &bfd_data->bfd, e_list) { + num_inst++; + if (bfd->local_state != BFD_STATE_UP) + num_down++; + } + + if (num_inst == 0) + state = STATUS_STATE_INIT; + else if (num_down > 0) + state = (num_down == num_inst) ? STATUS_STATE_FAULT : STATUS_STATE_DOWN; + else + state = STATUS_STATE_UP; + + status_send_bfd_event(state, num_inst, num_down); +} +#endif void bfd_event_send(bfd_t *bfd) @@ -82,4 +114,8 @@ bfd_event_send(bfd_t *bfd) bfd->iname); } #endif + +#ifdef _WITH_STATUS_SOCKET_ + bfd_send_status_event(); +#endif } diff --git a/keepalived/check/check_daemon.c b/keepalived/check/check_daemon.c index f78126f83f..2e7aa8f5c7 100644 --- a/keepalived/check/check_daemon.c +++ b/keepalived/check/check_daemon.c @@ -82,6 +82,9 @@ #ifndef _ONE_PROCESS_DEBUG_ #include "config_notify.h" #endif +#ifdef _WITH_STATUS_SOCKET_ +#include "status_event.h" +#endif /* Global variables */ bool using_ha_suspend; @@ -737,6 +740,35 @@ start_check_child(void) close_track_processes(); #endif +#ifdef _WITH_STATUS_SOCKET_ + /* Checker child keeps only write end of its status pipe */ + if (status_checker_pipe[0] >= 0) { + close(status_checker_pipe[0]); + status_checker_pipe[0] = -1; + } + /* Close all of VRRP and BFD status pipes */ +#ifdef _WITH_VRRP_ + if (status_vrrp_pipe[0] >= 0) { + close(status_vrrp_pipe[0]); + status_vrrp_pipe[0] = -1; + } + if (status_vrrp_pipe[1] >= 0) { + close(status_vrrp_pipe[1]); + status_vrrp_pipe[1] = -1; + } +#endif +#ifdef _WITH_BFD_ + if (status_bfd_pipe[0] >= 0) { + close(status_bfd_pipe[0]); + status_bfd_pipe[0] = -1; + } + if (status_bfd_pipe[1] >= 0) { + close(status_bfd_pipe[1]); + status_bfd_pipe[1] = -1; + } +#endif +#endif + if ((global_data->instance_name || global_data->network_namespace) && (check_syslog_ident = make_syslog_ident(PROG_CHECK))) syslog_ident = check_syslog_ident; diff --git a/keepalived/check/ipwrapper.c b/keepalived/check/ipwrapper.c index b5337bfacd..cb02473d1d 100644 --- a/keepalived/check/ipwrapper.c +++ b/keepalived/check/ipwrapper.c @@ -41,6 +41,41 @@ #include "check_nftables.h" #include "check_data.h" #endif +#ifdef _WITH_STATUS_SOCKET_ +#include "status_event.h" +#endif + +#ifdef _WITH_STATUS_SOCKET_ +static void +checker_send_status_event(void) +{ + virtual_server_t *vs; + real_server_t *rs; + uint32_t num_rs = 0; + uint32_t num_down = 0; + uint8_t state; + + if (!check_data) + return; + + list_for_each_entry(vs, &check_data->vs, e_list) { + list_for_each_entry(rs, &vs->rs, e_list) { + num_rs++; + if (!ISALIVE(rs)) + num_down++; + } + } + + if (num_rs == 0) + state = STATUS_STATE_INIT; + else if (num_down > 0) + state = (num_down == num_rs) ? STATUS_STATE_FAULT : STATUS_STATE_DOWN; + else + state = STATUS_STATE_UP; + + status_send_checker_event(state, num_rs, num_down); +} +#endif static bool __attribute((pure)) vs_iseq(const virtual_server_t *vs_a, const virtual_server_t *vs_b) @@ -621,6 +656,10 @@ perform_svr_state(bool alive, checker_t *checker) * but is now up, this is where the rs is added. */ update_quorum_state(vs, false); +#ifdef _WITH_STATUS_SOCKET_ + checker_send_status_event(); +#endif + return true; } diff --git a/keepalived/core/Makefile.am b/keepalived/core/Makefile.am index a588596aa2..ced781752c 100644 --- a/keepalived/core/Makefile.am +++ b/keepalived/core/Makefile.am @@ -50,4 +50,9 @@ if WITH_SANITIZER EXTRA_libcore_a_SOURCES += sanitizer.c endif +if WITH_STATUS_SOCKET + libcore_a_LIBADD += status_socket.o + EXTRA_libcore_a_SOURCES += status_socket.c +endif + MAINTAINERCLEANFILES = @MAINTAINERCLEANFILES@ diff --git a/keepalived/core/global_data.c b/keepalived/core/global_data.c index b35427cad0..d98963dde3 100644 --- a/keepalived/core/global_data.c +++ b/keepalived/core/global_data.c @@ -509,6 +509,9 @@ free_global_data(data_t **datap) FREE_CONST_PTR(data->dbus_service_name); FREE_CONST_PTR(data->dbus_no_interface_name); #endif +#ifdef _WITH_STATUS_SOCKET_ + FREE_CONST_PTR(data->status_socket_path); +#endif #ifndef _ONE_PROCESS_DEBUG_ FREE_CONST_PTR(data->reload_check_config); FREE_CONST_PTR(data->reload_file); @@ -920,6 +923,11 @@ dump_global_data(FILE *fp, data_t * data) conf_write(fp, " DBus %s", data->enable_dbus ? "enabled" : "disabled"); conf_write(fp, " DBus service name = %s", data->dbus_service_name ? data->dbus_service_name : ""); conf_write(fp, " DBus no interface name = %s", data->dbus_no_interface_name ? data->dbus_no_interface_name : dbus_no_interface_name); +#endif +#ifdef _WITH_STATUS_SOCKET_ + conf_write(fp, " Status socket %s", data->enable_status_socket ? "enabled" : "disabled"); + if (data->status_socket_path) + conf_write(fp, " Status socket path = %s", data->status_socket_path); #endif conf_write(fp, " Script security %s", script_security ? "enabled" : "disabled"); if (!get_default_script_user(&uid, &gid)) diff --git a/keepalived/core/global_parser.c b/keepalived/core/global_parser.c index d3e6c565c2..d323fcd857 100644 --- a/keepalived/core/global_parser.c +++ b/keepalived/core/global_parser.c @@ -1797,6 +1797,39 @@ dbus_no_interface_name_handler(const vector_t *strvec) } #endif +#ifdef _WITH_STATUS_SOCKET_ +static void +status_socket_handler(const vector_t *strvec) +{ + if (vector_size(strvec) < 2) { + report_config_error(CONFIG_GENERAL_ERROR, "status_socket requires path - ignoring"); + return; + } + + FREE_CONST_PTR(global_data->status_socket_path); + global_data->status_socket_path = set_value(strvec); + global_data->enable_status_socket = true; +} + +static void +status_socket_mode_handler(const vector_t *strvec) +{ + unsigned mode; + + if (vector_size(strvec) < 2) { + report_config_error(CONFIG_GENERAL_ERROR, "status_socket_mode requires mode value - ignoring"); + return; + } + + if (!read_unsigned(strvec_slot(strvec, 1), &mode, 0, 0777, true)) { + report_config_error(CONFIG_GENERAL_ERROR, "status_socket_mode %s invalid - ignoring", strvec_slot(strvec, 1)); + return; + } + + global_data->status_socket_mode = mode; +} +#endif + static void instance_handler(const vector_t *strvec) { @@ -2754,6 +2787,10 @@ init_global_keywords(bool global_active) install_keyword("enable_dbus", &enable_dbus_handler); install_keyword("dbus_service_name", &dbus_service_name_handler); install_keyword("dbus_no_interface_name", &dbus_no_interface_name_handler); +#endif +#ifdef _WITH_STATUS_SOCKET_ + install_keyword("status_socket", &status_socket_handler); + install_keyword("status_socket_mode", &status_socket_mode_handler); #endif install_keyword("script_user", &script_user_handler); install_keyword("enable_script_security", &script_security_handler); diff --git a/keepalived/core/main.c b/keepalived/core/main.c index 35e2d64d4b..eae64ef044 100644 --- a/keepalived/core/main.c +++ b/keepalived/core/main.c @@ -78,6 +78,9 @@ #include "bfd_daemon.h" #include "bfd_parser.h" #endif +#ifdef _WITH_STATUS_SOCKET_ +#include "status_socket.h" +#endif #include "global_parser.h" #include "namespaces.h" #include "scheduler.h" @@ -488,6 +491,11 @@ static void stop_keepalived(void) { #ifndef _ONE_PROCESS_DEBUG_ +#ifdef _WITH_STATUS_SOCKET_ + if (global_data && global_data->enable_status_socket) + status_socket_close(); +#endif + /* Just cleanup memory & exit */ thread_destroy_master(master); @@ -534,6 +542,16 @@ start_keepalived(__attribute__((unused)) thread_ref_t thread) } #endif +#ifdef _WITH_STATUS_SOCKET_ + /* Status pipes must be opened before children start */ + if (global_data->enable_status_socket) { + if (!open_status_pipes()) { + log_message(LOG_ERR, "Failed to create status pipes"); + /* Non-fatal, continue without status socket */ + } + } +#endif + #ifdef _WITH_LVS_ /* start healthchecker child */ if (running_checker()) { @@ -564,6 +582,14 @@ start_keepalived(__attribute__((unused)) thread_ref_t thread) children_started = true; +#ifdef _WITH_STATUS_SOCKET_ + /* Initialize status socket after children are forked */ + if (global_data->enable_status_socket) { + if (!status_socket_init(thread->master)) + log_message(LOG_ERR, "Failed to init status socket"); + } +#endif + #ifndef _ONE_PROCESS_DEBUG_ /* Do we have a reload file to monitor */ if (global_data->reload_time_file) diff --git a/keepalived/core/status_socket.c b/keepalived/core/status_socket.c new file mode 100644 index 0000000000..144ba49a2c --- /dev/null +++ b/keepalived/core/status_socket.c @@ -0,0 +1,587 @@ +/* + * Soft: Keepalived is a failover program for the LVS project + * . It monitor & manipulate + * a loadbalanced server pool using multi-layer checks. + * + * Part: Unix socket for unified status queries across all daemons. + * Runs in parent process, receives status updates from + * VRRP, Checker, and BFD child daemons via pipes. + * + * Author: Alexandre Cassen, + * + * 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 General Public License for more details. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + * Copyright (C) 2001-2024 Alexandre Cassen, + */ + +#include "config.h" + +#ifdef _WITH_STATUS_SOCKET_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "status_socket.h" +#include "status_event.h" +#include "scheduler.h" +#include "logger.h" +#include "global_data.h" +#include "memory.h" +#include "utils.h" +#include "main.h" + +#define STATUS_SOCKET_DEFAULT_PATH "/var/run/keepalived/status.sock" +#define STATUS_SOCKET_BACKLOG 5 +#define STATUS_SOCKET_TIMEOUT (5 * TIMER_HZ) +#define STATUS_SOCKET_MAX_REQUEST 64 +#define STATUS_SOCKET_MAX_RESPONSE 4096 + +/* Status pipes - defined here, declared extern in status_event.h */ +#ifdef _WITH_VRRP_ +int status_vrrp_pipe[2] = { -1, -1 }; +#endif +#ifdef _WITH_LVS_ +int status_checker_pipe[2] = { -1, -1 }; +#endif +#ifdef _WITH_BFD_ +int status_bfd_pipe[2] = { -1, -1 }; +#endif + +/* Unix socket fd */ +static int status_socket_fd = -1; + +/* Thread master reference */ +static thread_master_t *status_master; + +/* Aggregated state from each daemon */ +static struct daemon_state { + uint8_t state; + uint32_t num_instances; + uint32_t num_fault; + uint32_t num_master; + timeval_t last_update; + bool running; +} vrrp_state, checker_state, bfd_state; + +static const char * +state_to_string(uint8_t state) +{ + switch (state) { + case STATUS_STATE_INIT: + return "INIT"; + case STATUS_STATE_UP: + return "UP"; + case STATUS_STATE_DOWN: + return "DOWN"; + case STATUS_STATE_FAULT: + return "FAULT"; + default: + return "UNKNOWN"; + } +} + +static void +build_health_response(char *buf, size_t bufsize) +{ + bool any_fault = false; + bool any_running = false; + +#ifdef _WITH_VRRP_ + if (vrrp_state.running) { + any_running = true; + if (vrrp_state.state == STATUS_STATE_FAULT) + any_fault = true; + } +#endif +#ifdef _WITH_LVS_ + if (checker_state.running) { + any_running = true; + if (checker_state.state == STATUS_STATE_FAULT) + any_fault = true; + } +#endif +#ifdef _WITH_BFD_ + if (bfd_state.running) { + any_running = true; + if (bfd_state.state == STATUS_STATE_FAULT) + any_fault = true; + } +#endif + + if (!any_running) { + snprintf(buf, bufsize, "UNKNOWN\n"); + return; + } + + if (any_fault) { + snprintf(buf, bufsize, "FAULT\n"); + return; + } + +#ifdef _WITH_VRRP_ + if (vrrp_state.running && vrrp_state.num_master > 0) { + snprintf(buf, bufsize, "MASTER\n"); + return; + } +#endif + + snprintf(buf, bufsize, "BACKUP\n"); +} + +static void +build_status_response(char *buf, size_t bufsize) +{ + int len; + size_t remaining = bufsize; + char *p = buf; + + len = snprintf(p, remaining, "{"); + if (len < 0 || (size_t)len >= remaining) + return; + p += len; + remaining -= len; + + bool first = true; + +#ifdef _WITH_VRRP_ + if (vrrp_state.running) { + len = snprintf(p, remaining, + "%s\"vrrp\":{\"state\":\"%s\",\"instances\":%u,\"fault\":%u,\"master\":%u}", + first ? "" : ",", + state_to_string(vrrp_state.state), + vrrp_state.num_instances, + vrrp_state.num_fault, + vrrp_state.num_master); + if (len < 0 || (size_t)len >= remaining) + return; + p += len; + remaining -= len; + first = false; + } +#endif + +#ifdef _WITH_LVS_ + if (checker_state.running) { + len = snprintf(p, remaining, + "%s\"checker\":{\"state\":\"%s\",\"instances\":%u,\"fault\":%u}", + first ? "" : ",", + state_to_string(checker_state.state), + checker_state.num_instances, + checker_state.num_fault); + if (len < 0 || (size_t)len >= remaining) + return; + p += len; + remaining -= len; + first = false; + } +#endif + +#ifdef _WITH_BFD_ + if (bfd_state.running) { + len = snprintf(p, remaining, + "%s\"bfd\":{\"state\":\"%s\",\"instances\":%u,\"fault\":%u}", + first ? "" : ",", + state_to_string(bfd_state.state), + bfd_state.num_instances, + bfd_state.num_fault); + if (len < 0 || (size_t)len >= remaining) + return; + p += len; + remaining -= len; + } +#endif + + snprintf(p, remaining, "}\n"); +} + +static void +handle_client_request(thread_ref_t thread) +{ + int client_fd = thread->u.f.fd; + char request[STATUS_SOCKET_MAX_REQUEST]; + char response[STATUS_SOCKET_MAX_RESPONSE]; + ssize_t n; + + n = read(client_fd, request, sizeof(request) - 1); + if (n <= 0) { + close(client_fd); + return; + } + request[n] = '\0'; + + /* Remove trailing newline */ + while (n > 0 && (request[n - 1] == '\n' || request[n - 1] == '\r')) + request[--n] = '\0'; + + if (strncmp(request, "HEALTH", 6) == 0) { + build_health_response(response, sizeof(response)); + } else if (strncmp(request, "STATUS", 6) == 0) { + build_status_response(response, sizeof(response)); + } else { + snprintf(response, sizeof(response), + "ERROR: Unknown command. Use HEALTH or STATUS.\n"); + } + + if (write(client_fd, response, strlen(response)) < 0) + log_message(LOG_INFO, "Status socket: write to client failed - %s", + strerror(errno)); + + close(client_fd); +} + +static void +accept_client_connection(thread_ref_t thread) +{ + int client_fd; + + client_fd = accept(status_socket_fd, NULL, NULL); + if (client_fd >= 0) { + fcntl(client_fd, F_SETFL, O_NONBLOCK | fcntl(client_fd, F_GETFL)); + thread_add_read(status_master, handle_client_request, NULL, + client_fd, STATUS_SOCKET_TIMEOUT, 0); + } + + /* Re-register for next connection */ + thread_add_read(status_master, accept_client_connection, NULL, + status_socket_fd, TIMER_NEVER, 0); +} + +#ifdef _WITH_VRRP_ +static void +read_vrrp_status_pipe(thread_ref_t thread) +{ + status_event_t evt; + ssize_t n; + + n = read(thread->u.f.fd, &evt, sizeof(evt)); + if (n == sizeof(evt)) { + vrrp_state.state = evt.overall_state; + vrrp_state.num_instances = evt.num_instances; + vrrp_state.num_fault = evt.num_fault; + vrrp_state.num_master = evt.num_master; + vrrp_state.last_update = evt.sent_time; + vrrp_state.running = true; + } + + /* Re-register for next event */ + thread_add_read(status_master, read_vrrp_status_pipe, NULL, + status_vrrp_pipe[0], TIMER_NEVER, 0); +} +#endif + +#ifdef _WITH_LVS_ +static void +read_checker_status_pipe(thread_ref_t thread) +{ + status_event_t evt; + ssize_t n; + + n = read(thread->u.f.fd, &evt, sizeof(evt)); + if (n == sizeof(evt)) { + checker_state.state = evt.overall_state; + checker_state.num_instances = evt.num_instances; + checker_state.num_fault = evt.num_fault; + checker_state.last_update = evt.sent_time; + checker_state.running = true; + } + + /* Re-register for next event */ + thread_add_read(status_master, read_checker_status_pipe, NULL, + status_checker_pipe[0], TIMER_NEVER, 0); +} +#endif + +#ifdef _WITH_BFD_ +static void +read_bfd_status_pipe(thread_ref_t thread) +{ + status_event_t evt; + ssize_t n; + + n = read(thread->u.f.fd, &evt, sizeof(evt)); + if (n == sizeof(evt)) { + bfd_state.state = evt.overall_state; + bfd_state.num_instances = evt.num_instances; + bfd_state.num_fault = evt.num_fault; + bfd_state.last_update = evt.sent_time; + bfd_state.running = true; + } + + /* Re-register for next event */ + thread_add_read(status_master, read_bfd_status_pipe, NULL, + status_bfd_pipe[0], TIMER_NEVER, 0); +} +#endif + +bool +open_status_pipes(void) +{ +#ifdef _WITH_VRRP_ + if (running_vrrp()) { + if (open_pipe(status_vrrp_pipe) == -1) { + log_message(LOG_ERR, "Status socket: Unable to create VRRP status pipe: %m"); + return false; + } + } +#endif + +#ifdef _WITH_LVS_ + if (running_checker()) { + if (open_pipe(status_checker_pipe) == -1) { + log_message(LOG_ERR, "Status socket: Unable to create checker status pipe: %m"); + return false; + } + } +#endif + +#ifdef _WITH_BFD_ + if (running_bfd()) { + if (open_pipe(status_bfd_pipe) == -1) { + log_message(LOG_ERR, "Status socket: Unable to create BFD status pipe: %m"); + return false; + } + } +#endif + + return true; +} + +void +close_status_write_pipes(void) +{ + /* Close write ends in parent - children write, parent reads */ +#ifdef _WITH_VRRP_ + if (status_vrrp_pipe[1] >= 0) { + close(status_vrrp_pipe[1]); + status_vrrp_pipe[1] = -1; + } +#endif +#ifdef _WITH_LVS_ + if (status_checker_pipe[1] >= 0) { + close(status_checker_pipe[1]); + status_checker_pipe[1] = -1; + } +#endif +#ifdef _WITH_BFD_ + if (status_bfd_pipe[1] >= 0) { + close(status_bfd_pipe[1]); + status_bfd_pipe[1] = -1; + } +#endif +} + +bool +status_socket_init(thread_master_t *m) +{ + struct sockaddr_un addr; + const char *path; + mode_t mode; + + status_master = m; + path = global_data->status_socket_path ? global_data->status_socket_path + : STATUS_SOCKET_DEFAULT_PATH; + mode = global_data->status_socket_mode ? global_data->status_socket_mode : 0600; + + /* Remove stale socket */ + unlink(path); + + status_socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); + if (status_socket_fd < 0) { + log_message(LOG_INFO, "Status socket: failed to create socket - %s", + strerror(errno)); + return false; + } + + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); + + if (bind(status_socket_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + log_message(LOG_INFO, "Status socket: failed to bind to %s - %s", + path, strerror(errno)); + close(status_socket_fd); + status_socket_fd = -1; + return false; + } + + if (chmod(path, mode) < 0) + log_message(LOG_INFO, "Status socket: failed to set permissions on %s - %s", + path, strerror(errno)); + + if (listen(status_socket_fd, STATUS_SOCKET_BACKLOG) < 0) { + log_message(LOG_INFO, "Status socket: failed to listen on %s - %s", + path, strerror(errno)); + close(status_socket_fd); + status_socket_fd = -1; + unlink(path); + return false; + } + + /* Close write ends of pipes - parent only reads */ + close_status_write_pipes(); + + /* Register pipe readers for each daemon */ +#ifdef _WITH_VRRP_ + if (status_vrrp_pipe[0] >= 0) + thread_add_read(status_master, read_vrrp_status_pipe, NULL, + status_vrrp_pipe[0], TIMER_NEVER, 0); +#endif +#ifdef _WITH_LVS_ + if (status_checker_pipe[0] >= 0) + thread_add_read(status_master, read_checker_status_pipe, NULL, + status_checker_pipe[0], TIMER_NEVER, 0); +#endif +#ifdef _WITH_BFD_ + if (status_bfd_pipe[0] >= 0) + thread_add_read(status_master, read_bfd_status_pipe, NULL, + status_bfd_pipe[0], TIMER_NEVER, 0); +#endif + + /* Register socket accept handler */ + thread_add_read(status_master, accept_client_connection, NULL, + status_socket_fd, TIMER_NEVER, 0); + + log_message(LOG_INFO, "Status socket listening on %s", path); + return true; +} + +void +status_socket_close(void) +{ + const char *path; + + if (status_socket_fd >= 0) { + close(status_socket_fd); + status_socket_fd = -1; + } + + /* Close read ends of pipes */ +#ifdef _WITH_VRRP_ + if (status_vrrp_pipe[0] >= 0) { + close(status_vrrp_pipe[0]); + status_vrrp_pipe[0] = -1; + } +#endif +#ifdef _WITH_LVS_ + if (status_checker_pipe[0] >= 0) { + close(status_checker_pipe[0]); + status_checker_pipe[0] = -1; + } +#endif +#ifdef _WITH_BFD_ + if (status_bfd_pipe[0] >= 0) { + close(status_bfd_pipe[0]); + status_bfd_pipe[0] = -1; + } +#endif + + if (global_data && global_data->status_socket_path) + path = global_data->status_socket_path; + else + path = STATUS_SOCKET_DEFAULT_PATH; + + unlink(path); +} + +/* Functions for children to send status events to parent */ +#ifdef _WITH_VRRP_ +void +status_send_vrrp_event(uint8_t state, uint32_t num_inst, + uint32_t num_fault, uint32_t num_master) +{ + status_event_t evt; + + if (status_vrrp_pipe[1] < 0) + return; + + memset(&evt, 0, sizeof(evt)); + evt.daemon_type = STATUS_DAEMON_VRRP; + evt.overall_state = state; + evt.num_instances = num_inst; + evt.num_fault = num_fault; + evt.num_master = num_master; + evt.sent_time = timer_now(); + + if (write(status_vrrp_pipe[1], &evt, sizeof(evt)) < 0 && + __test_bit(LOG_DETAIL_BIT, &debug)) + log_message(LOG_ERR, "Status socket: VRRP pipe write error - %m"); +} +#endif + +#ifdef _WITH_LVS_ +void +status_send_checker_event(uint8_t state, uint32_t num_inst, uint32_t num_fault) +{ + status_event_t evt; + + if (status_checker_pipe[1] < 0) + return; + + memset(&evt, 0, sizeof(evt)); + evt.daemon_type = STATUS_DAEMON_CHECKER; + evt.overall_state = state; + evt.num_instances = num_inst; + evt.num_fault = num_fault; + evt.sent_time = timer_now(); + + if (write(status_checker_pipe[1], &evt, sizeof(evt)) < 0 && + __test_bit(LOG_DETAIL_BIT, &debug)) + log_message(LOG_ERR, "Status socket: Checker pipe write error - %m"); +} +#endif + +#ifdef _WITH_BFD_ +void +status_send_bfd_event(uint8_t state, uint32_t num_inst, uint32_t num_fault) +{ + status_event_t evt; + + if (status_bfd_pipe[1] < 0) + return; + + memset(&evt, 0, sizeof(evt)); + evt.daemon_type = STATUS_DAEMON_BFD; + evt.overall_state = state; + evt.num_instances = num_inst; + evt.num_fault = num_fault; + evt.sent_time = timer_now(); + + if (write(status_bfd_pipe[1], &evt, sizeof(evt)) < 0 && + __test_bit(LOG_DETAIL_BIT, &debug)) + log_message(LOG_ERR, "Status socket: BFD pipe write error - %m"); +} +#endif + +#ifdef THREAD_DUMP +void +register_status_socket_addresses(void) +{ + register_thread_address("status_socket_accept", accept_client_connection); + register_thread_address("status_socket_client", handle_client_request); +#ifdef _WITH_VRRP_ + register_thread_address("status_vrrp_pipe_read", read_vrrp_status_pipe); +#endif +#ifdef _WITH_LVS_ + register_thread_address("status_checker_pipe_read", read_checker_status_pipe); +#endif +#ifdef _WITH_BFD_ + register_thread_address("status_bfd_pipe_read", read_bfd_status_pipe); +#endif +} +#endif + +#endif /* _WITH_STATUS_SOCKET_ */ diff --git a/keepalived/include/global_data.h b/keepalived/include/global_data.h index 25b7d92c18..71ec1c1d45 100644 --- a/keepalived/include/global_data.h +++ b/keepalived/include/global_data.h @@ -260,6 +260,11 @@ typedef struct _data { const char *dbus_service_name; const char *dbus_no_interface_name; #endif +#ifdef _WITH_STATUS_SOCKET_ + bool enable_status_socket; + const char *status_socket_path; + mode_t status_socket_mode; +#endif #ifdef _WITH_VRRP_ unsigned vrrp_netlink_cmd_rcv_bufs; bool vrrp_netlink_cmd_rcv_bufs_force; diff --git a/keepalived/include/status_event.h b/keepalived/include/status_event.h new file mode 100644 index 0000000000..113a4afbe0 --- /dev/null +++ b/keepalived/include/status_event.h @@ -0,0 +1,82 @@ +/* + * Soft: Keepalived is a failover program for the LVS project + * . It monitor & manipulate + * a loadbalanced server pool using multi-layer checks. + * + * Part: Status event structure for IPC between child daemons + * and parent process status socket. + * + * Author: Alexandre Cassen, + * + * 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 General Public License for more details. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + * Copyright (C) 2001-2024 Alexandre Cassen, + */ + +#ifndef _STATUS_EVENT_H +#define _STATUS_EVENT_H + +#include "config.h" + +#ifdef _WITH_STATUS_SOCKET_ + +#include +#include +#include "timer.h" + +/* Daemon types for status events */ +#define STATUS_DAEMON_VRRP 1 +#define STATUS_DAEMON_CHECKER 2 +#define STATUS_DAEMON_BFD 3 + +/* Overall daemon states */ +#define STATUS_STATE_INIT 0 +#define STATUS_STATE_UP 1 +#define STATUS_STATE_DOWN 2 +#define STATUS_STATE_FAULT 3 + +/* + * Status event structure sent from child daemons to parent. + * Children write this to their status pipe on state changes. + * Parent reads and aggregates for unified health reporting. + */ +typedef struct _status_event { + uint8_t daemon_type; /* STATUS_DAEMON_VRRP, etc. */ + uint8_t overall_state; /* STATUS_STATE_* */ + uint8_t pad[2]; /* Alignment padding */ + uint32_t num_instances; /* Total instance count */ + uint32_t num_fault; /* Instances in fault state */ + uint32_t num_master; /* VRRP only: instances in MASTER */ + timeval_t sent_time; /* Timestamp for latency tracking */ +} status_event_t; + +/* Status pipes - one per daemon type, defined in status_socket.c */ +#ifdef _WITH_VRRP_ +extern int status_vrrp_pipe[2]; +#endif +#ifdef _WITH_LVS_ +extern int status_checker_pipe[2]; +#endif +#ifdef _WITH_BFD_ +extern int status_bfd_pipe[2]; +#endif + +/* Functions for child daemons to send status updates */ +extern void status_send_vrrp_event(uint8_t state, uint32_t num_inst, + uint32_t num_fault, uint32_t num_master); +extern void status_send_checker_event(uint8_t state, uint32_t num_inst, + uint32_t num_fault); +extern void status_send_bfd_event(uint8_t state, uint32_t num_inst, + uint32_t num_fault); + +#endif /* _WITH_STATUS_SOCKET_ */ + +#endif /* _STATUS_EVENT_H */ diff --git a/keepalived/include/status_socket.h b/keepalived/include/status_socket.h new file mode 100644 index 0000000000..c9eaea0d67 --- /dev/null +++ b/keepalived/include/status_socket.h @@ -0,0 +1,50 @@ +/* + * Soft: Keepalived is a failover program for the LVS project + * . It monitor & manipulate + * a loadbalanced server pool using multi-layer checks. + * + * Part: status_socket.c include file. + * + * Author: Alexandre Cassen, + * + * 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 General Public License for more details. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version + * 2 of the License, or (at your option) any later version. + * + * Copyright (C) 2001-2024 Alexandre Cassen, + */ + +#ifndef _STATUS_SOCKET_H +#define _STATUS_SOCKET_H + +#include "config.h" + +#ifdef _WITH_STATUS_SOCKET_ + +/* System includes */ +#include + +/* Local includes */ +#include "scheduler.h" + +/* Pipe management - called from main.c */ +extern bool open_status_pipes(void); +extern void close_status_write_pipes(void); + +/* Socket management - called from main.c after children start */ +extern bool status_socket_init(thread_master_t *); +extern void status_socket_close(void); + +#ifdef THREAD_DUMP +extern void register_status_socket_addresses(void); +#endif + +#endif /* _WITH_STATUS_SOCKET_ */ + +#endif /* _STATUS_SOCKET_H */ diff --git a/keepalived/vrrp/vrrp_daemon.c b/keepalived/vrrp/vrrp_daemon.c index 8d2e5f95b9..b491588707 100644 --- a/keepalived/vrrp/vrrp_daemon.c +++ b/keepalived/vrrp/vrrp_daemon.c @@ -66,6 +66,10 @@ #ifdef _WITH_DBUS_ #include "vrrp_dbus.h" #endif +#ifdef _WITH_STATUS_SOCKET_ + #include "status_socket.h" + #include "status_event.h" +#endif #include "list_head.h" #include "main.h" #include "parser.h" @@ -316,6 +320,11 @@ vrrp_terminate_phase2(int exit_status) dbus_stop(); #endif +#ifdef _WITH_STATUS_SOCKET_ + if (global_data->enable_status_socket) + status_socket_close(); +#endif + clear_rt_names(); if (global_data->vrrp_notify_fifo.fd != -1) @@ -700,6 +709,17 @@ start_vrrp(data_t *prev_global_data) dbus_stop(); #endif +#ifdef _WITH_STATUS_SOCKET_ + if (global_data->enable_status_socket) { + if (reload && old_global_data->enable_status_socket) + status_socket_close(); + if (!status_socket_init(master)) + global_data->enable_status_socket = false; + } + else if (reload && old_global_data->enable_status_socket) + status_socket_close(); +#endif + /* Set static entries */ netlink_iplist(&vrrp_data->static_addresses, IPADDRESS_ADD, false); netlink_rtlist(&vrrp_data->static_routes, IPROUTE_ADD, false); @@ -994,6 +1014,9 @@ register_vrrp_thread_addresses(void) register_vrrp_scheduler_addresses(); #ifdef _WITH_DBUS_ register_vrrp_dbus_addresses(); +#endif +#ifdef _WITH_STATUS_SOCKET_ + register_status_socket_addresses(); #endif register_vrrp_fifo_addresses(); register_track_file_inotify_addresses(); @@ -1098,6 +1121,35 @@ start_vrrp_child(void) close(bfd_checker_event_pipe[0]); close(bfd_checker_event_pipe[1]); #endif +#endif + +#ifdef _WITH_STATUS_SOCKET_ + /* VRRP child keeps only write end of its status pipe */ + if (status_vrrp_pipe[0] >= 0) { + close(status_vrrp_pipe[0]); + status_vrrp_pipe[0] = -1; + } + /* Close all of checker and BFD status pipes */ +#ifdef _WITH_LVS_ + if (status_checker_pipe[0] >= 0) { + close(status_checker_pipe[0]); + status_checker_pipe[0] = -1; + } + if (status_checker_pipe[1] >= 0) { + close(status_checker_pipe[1]); + status_checker_pipe[1] = -1; + } +#endif +#ifdef _WITH_BFD_ + if (status_bfd_pipe[0] >= 0) { + close(status_bfd_pipe[0]); + status_bfd_pipe[0] = -1; + } + if (status_bfd_pipe[1] >= 0) { + close(status_bfd_pipe[1]); + status_bfd_pipe[1] = -1; + } +#endif #endif /* Opening local VRRP syslog channel */ diff --git a/keepalived/vrrp/vrrp_notify.c b/keepalived/vrrp/vrrp_notify.c index 6047ce7fd0..48f9ee5f0f 100644 --- a/keepalived/vrrp/vrrp_notify.c +++ b/keepalived/vrrp/vrrp_notify.c @@ -40,6 +40,10 @@ #include "vrrp_snmp.h" #endif #include "smtp.h" +#ifdef _WITH_STATUS_SOCKET_ +#include "status_event.h" +#include "list_head.h" +#endif static notify_script_t* get_iscript(vrrp_t * vrrp) @@ -266,6 +270,38 @@ send_event_notify(vrrp_t *vrrp, int event) notify_fifo(vrrp->iname, event, false, vrrp->effective_priority); } +#ifdef _WITH_STATUS_SOCKET_ +static void +vrrp_send_status_event(void) +{ + vrrp_t *vrrp; + uint32_t num_inst = 0; + uint32_t num_fault = 0; + uint32_t num_master = 0; + uint8_t state; + + if (!vrrp_data || list_empty(&vrrp_data->vrrp)) + return; + + list_for_each_entry(vrrp, &vrrp_data->vrrp, e_list) { + num_inst++; + if (vrrp->state == VRRP_STATE_FAULT) + num_fault++; + if (vrrp->state == VRRP_STATE_MAST) + num_master++; + } + + if (num_fault > 0) + state = STATUS_STATE_FAULT; + else if (num_master > 0) + state = STATUS_STATE_UP; + else + state = STATUS_STATE_DOWN; + + status_send_vrrp_event(state, num_inst, num_fault, num_master); +} +#endif + void send_instance_notifies(vrrp_t *vrrp) { @@ -317,6 +353,10 @@ send_instance_notifies(vrrp_t *vrrp) #endif } vrrp_smtp_notifier(vrrp); + +#ifdef _WITH_STATUS_SOCKET_ + vrrp_send_status_event(); +#endif } void