From 5c5c52b3ceea61b940d09f7753ca8581f4e67699 Mon Sep 17 00:00:00 2001 From: InterLinked1 <24227567+InterLinked1@users.noreply.github.com> Date: Tue, 18 Feb 2025 07:45:04 -0500 Subject: [PATCH] mod_systemd: Add systemd support. Add systemd support, via an optional module, to avoid adding systemd dependencies to the core. This allows the BBS to be easily managed as a service, like most other daemon services, and automatically started and restarted if needed. Apart from helping ensure BBS uptime, the service file also automatically ensures the BBS can run on privileged ports which non-root users normally would not be able to bind to. Apart from crafting the appropriate service file, some minor changes were needed to allow this: * Add an event for restart, allowing a SIGUSR2 sent to the BBS to trigger a configuration reload (via reload handlers) * Don't prompt for confirmation on SIGTERM, only on SIGINT. This ensures systemd can stop the BBS successfully when it sends SIGTERM to stop. Only SIGINT is expected to be used interactively anyways. * Since systemd starts the BBS as the run user directly, rather than the BBS starting as root and dropping privileges after certain key operations, these key operations need to be done in the service file using ExecStartPre. Additionally, these key operations can fail in bbs.c if their failure is harmless (since the operation was already performed), so recognizing that, some of these checks are a little more careful now about aborting startup. --- .github/workflows/main.yml | 16 +++++++++ Makefile | 7 ++++ README.rst | 6 ++++ bbs/bbs.c | 71 +++++++++++++++++++++++++++----------- bbs/event.c | 3 ++ bbs/module.c | 19 +++++++++- configs/lbbs.service | 53 ++++++++++++++++++++++++++++ configs/modules.conf | 3 ++ include/event.h | 5 +-- include/reload.h | 8 +++++ modules/Makefile | 6 +++- modules/mod_systemd.c | 55 +++++++++++++++++++++++++++++ scripts/install_prereq.sh | 8 +++++ 13 files changed, 236 insertions(+), 24 deletions(-) create mode 100755 configs/lbbs.service create mode 100755 modules/mod_systemd.c diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 709a75bf..6fb51ba5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,12 @@ jobs: sudo tests/test -ddddddddd -DDDDDDDDDD -x sudo apt-get install -y valgrind sudo tests/test -ddddddddd -DDDDDDDDDD -ex + - name: Install service + run: | + sudo adduser -c "BBS" bbs --disabled-password --shell /usr/sbin/nologin --gecos "" + # Remove the bbs.log from test suite executions since that is owned by root + sudo rm /var/log/lbbs/bbs.log + sudo make service ubuntu-stable: runs-on: ubuntu-22.04 name: Ubuntu 22.04 @@ -50,6 +56,12 @@ jobs: sudo tests/test -ddddddddd -DDDDDDDDDD -x sudo apt-get install -y valgrind sudo tests/test -ddddddddd -DDDDDDDDDD -ex + - name: Install service + run: | + sudo adduser -c "BBS" bbs --disabled-password --shell /usr/sbin/nologin --gecos "" + # Remove the bbs.log from test suite executions since that is owned by root + sudo rm /var/log/lbbs/bbs.log + sudo make service without-optimization: runs-on: ubuntu-22.04 name: Ubuntu 22.04, without optimization @@ -66,6 +78,10 @@ jobs: sudo make install sudo make samples sudo make tests + - name: Install service + run: | + sudo adduser -c "BBS" bbs --disabled-password --shell /usr/sbin/nologin --gecos "" + sudo make service debian-12: runs-on: ubuntu-24.04 name: Debian 12 diff --git a/Makefile b/Makefile index 498091a8..f897e999 100644 --- a/Makefile +++ b/Makefile @@ -173,6 +173,13 @@ samples : templates fi cp -n configs/*.conf /etc/lbbs +# Don't allow the service to be installed if the module couldn't be built +service : modules/mod_systemd.so + $(INSTALL) -m 644 configs/lbbs.service "/etc/systemd/system/lbbs.service" + systemctl enable lbbs.service + # Even if the BBS is already running, this will return 0 + systemctl start lbbs + doxygen : # apt-get install -y doxygen graphviz doxygen Doxyfile.in diff --git a/README.rst b/README.rst index 925a5ae6..4d987421 100644 --- a/README.rst +++ b/README.rst @@ -99,6 +99,7 @@ To install LBBS, you will need to compile it from source. Fortunately, we've mad make make install make samples + make service (Running :code:`make modcheck` is optional. It will tell you all the modules that are available and which will be disabled for the current build. Running :code:`make modconfig` is what actually makes changes to the build environment, disabling any modules with unmet dependencies.) @@ -257,6 +258,11 @@ Finally, note that many systems already have daemons running on the standard por sshd, telnetd, Apache web server, etc. If these are present, you will need to resolve the conflict, as only one program can bind to a port at any given time. +How do I run the BBS as a service under systemd? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run :code:`make service`, and this will install the service file for systemd to use. + Can I run SSH and SFTP on the same port? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/bbs/bbs.c b/bbs/bbs.c index a08dc588..2a9e2329 100644 --- a/bbs/bbs.c +++ b/bbs/bbs.c @@ -52,6 +52,7 @@ #include #include "include/module.h" /* use load_modules */ +#include "include/reload.h" #include "include/alertpipe.h" #include "include/os.h" #include "include/system.h" @@ -298,8 +299,10 @@ static int run_init(int argc, char *argv[]) return -1; } if (chown(BBS_RUN_DIR, pw->pw_uid, -1)) { - fprintf(stderr, "Unable to chown run directory to %d (%s)\n", (int) pw->pw_uid, runuser); - return -1; + if (eaccess(BBS_RUN_DIR, X_OK)) { /* If we don't already have the right permissions, then that's an issue */ + fprintf(stderr, "Unable to chown run directory to %d (%s)\n", (int) pw->pw_uid, runuser); + return -1; + } } if (eaccess(BBS_LOG_DIR, R_OK)) { if (mkdir(BBS_LOG_DIR, 0744)) { /* Directory must be executable to be able to create files in it */ @@ -617,8 +620,6 @@ static void __sigint_handler(int num) { time_t now; - UNUSED(num); - if (getpid() != bbs_pid) { /* __sigint_handler triggered from child process before it was removed in system.c (race condition). Just abort. * Remember, must NEVER call bbs_log within a child, it will deadlock waiting for the log lock. */ @@ -649,21 +650,27 @@ static void __sigint_handler(int num) return; } now = time(NULL); - if (!last_shutdown_attempt || last_shutdown_attempt < now - 10) { - /* ^C in a remote console just exits the console, while - * ^C in the foreground console terminates the BBS. - * It is easy to forget which console you are in and - * accidentally kill the entire BBS when you just meant to exit the console. - * Try to prevent this from happening by asking if that's what the sysop really wants. */ - last_shutdown_attempt = now; - if (!strlen_zero(bbs_hostname())) { - printf("\n%sReally shut down the BBS on %s? Press ^C again within 10s to confirm.%s\n", COLOR(TERM_COLOR_RED), bbs_hostname(), COLOR_RESET); /* XXX technically not safe to use in signal handler */ - } else { - printf("\n%sReally shut down the BBS? Press ^C again within 10s to confirm.%s\n", COLOR(TERM_COLOR_RED), COLOR_RESET); /* XXX technically not safe to use in signal handler */ + + /* Prompt for confirmation with SIGINT. + * No confirmation for SIGTERM. */ + if (num == SIGINT) { + if (!last_shutdown_attempt || last_shutdown_attempt < now - 10) { + /* ^C in a remote console just exits the console, while + * ^C in the foreground console terminates the BBS. + * It is easy to forget which console you are in and + * accidentally kill the entire BBS when you just meant to exit the console. + * Try to prevent this from happening by asking if that's what the sysop really wants. */ + last_shutdown_attempt = now; + if (!strlen_zero(bbs_hostname())) { + printf("\n%sReally shut down the BBS on %s? Press ^C again within 10s to confirm.%s\n", COLOR(TERM_COLOR_RED), bbs_hostname(), COLOR_RESET); /* XXX technically not safe to use in signal handler */ + } else { + printf("\n%sReally shut down the BBS? Press ^C again within 10s to confirm.%s\n", COLOR(TERM_COLOR_RED), COLOR_RESET); /* XXX technically not safe to use in signal handler */ + } + return; } - return; + bbs_debug(2, "Got SIGINT, requesting shutdown\n"); /* XXX technically not safe to use in signal handler */ } - bbs_debug(2, "Got SIGINT, requesting shutdown\n"); /* XXX technically not safe to use in signal handler */ + want_shutdown = 1; if (bbs_alertpipe_write(sig_alert_pipe) < 0) { /* Don't use BBS log functions within a signal handler */ @@ -734,6 +741,19 @@ static struct sigaction sigusr1_handler = { .sa_handler = __sigusr1_handler, }; +static void __sigusr2_handler(int num) +{ + UNUSED(num); + + /* Execute reload handlers */ +#define ALL_RELOAD_HANDLER_INDICATOR "*" + bbs_request_module_unload(ALL_RELOAD_HANDLER_INDICATOR, 1); /* The 2nd argument doesn't matter in this case */ +} + +static struct sigaction sigusr2_handler = { + .sa_handler = __sigusr2_handler, +}; + /*! * \brief Request BBS shutdown * \param type @@ -811,8 +831,18 @@ static void *monitor_sig_flags(void *unused) bbs_mutex_lock(&sig_lock); bbs_alertpipe_read(sig_alert_pipe); if (task_modulename[0]) { - bbs_debug(1, "Asynchronously %s module '%s'\n", task_reload ? "reloading" : "unloading", task_modulename); - task_reload ? bbs_module_reload(task_modulename, 0) : bbs_module_unload(task_modulename); + if (!strcmp(task_modulename, ALL_RELOAD_HANDLER_INDICATOR)) { + /* Reload configuration + * Note: This does NOT reload all modules, + * it only executes all registered reload handlers + * to reload the configuration in those modules. */ + bbs_debug(1, "Asynchronously executing all reload handlers\n"); + bbs_reload(NULL, -1); + } else { + /* Reload a specific module */ + bbs_debug(1, "Asynchronously %s module '%s'\n", task_reload ? "reloading" : "unloading", task_modulename); + task_reload ? bbs_module_reload(task_modulename, 0) : bbs_module_unload(task_modulename); + } task_modulename[0] = '\0'; bbs_mutex_unlock(&sig_lock); } else if (want_shutdown) { @@ -941,7 +971,7 @@ static void set_signals(void) { /* Use sigaction instead of signal, since it's the more modern and well-behaving API: */ sigaction(SIGINT, &sigint_handler, NULL); - sigaction(SIGTERM, &sigint_handler, NULL); + sigaction(SIGTERM, &sigint_handler, NULL); /* Almost identical handling, but without a confirmation */ if (!option_nofork) { /* If daemonized, we get a SIGHUP whenever a remote sysop console disconnects... ignore it, * or the BBS will get killed by the signal. @@ -963,6 +993,7 @@ static void set_signals(void) } sigaction(SIGWINCH, &sigwinch_handler, NULL); sigaction(SIGUSR1, &sigusr1_handler, NULL); + sigaction(SIGUSR2, &sigusr2_handler, NULL); /* Used for reload, since SIGHUP is ignored */ sigaction(SIGPIPE, &ignore_sig_handler, NULL); } diff --git a/bbs/event.c b/bbs/event.c index 75dd105d..113762f1 100644 --- a/bbs/event.c +++ b/bbs/event.c @@ -85,6 +85,8 @@ const char *bbs_event_name(enum bbs_event_type type) return "STARTUP"; case EVENT_SHUTDOWN: return "SHUTDOWN"; + case EVENT_RELOAD: + return "RELOAD"; case EVENT_NODE_SHORT_SESSION: return "NODE_SHORT_SESSION"; case EVENT_NODE_ENCRYPTION_FAILED: @@ -156,6 +158,7 @@ int bbs_event_dispatch_custom(struct bbs_node *node, enum bbs_event_type type, c switch (type) { case EVENT_STARTUP: case EVENT_SHUTDOWN: + case EVENT_RELOAD: break; case EVENT_USER_REGISTRATION: case EVENT_USER_LOGIN: diff --git a/bbs/module.c b/bbs/module.c index eb964a00..b9f72f1d 100644 --- a/bbs/module.c +++ b/bbs/module.c @@ -36,6 +36,7 @@ #include "include/utils.h" /* use bbs_dir_traverse */ #include "include/node.h" #include "include/cli.h" +#include "include/event.h" #define BBS_MODULE_DIR DIRCAT("/usr/lib", DIRCAT(BBS_NAME, "modules")) @@ -1578,12 +1579,21 @@ static int cli_reloadhandlers(struct bbs_cli_args *a) return 0; } -static int reload_core(const char *name, int fd) +int bbs_reload(const char *name, int fd) { int res = 0; int reloaded = 0; struct reload_handler *r; + /* If the reload was triggered by the main thread, + * (as opposed to a reload initiated by a console), + * then there isn't anywhere the output needs to (or should) go. + * We can send it to STDOUT, even if we are daemonized, + * that will just get safely discarded. */ + if (fd == -1) { + fd = STDOUT_FILENO; + } + if (bbs_is_shutting_down()) { /* Can't reload if shutting down, particularly as * some stuff in the core will start unregistering @@ -1607,10 +1617,17 @@ static int reload_core(const char *name, int fd) } } RWLIST_UNLOCK(&reload_handlers); + bbs_event_dispatch(NULL, EVENT_RELOAD); if (!res && reloaded) { /* We reloaded at least one thing, and everything reloaded successfully */ return 0; } + return 1; +} + +static int reload_core(const char *name, int fd) +{ + int res = bbs_reload(name, fd); if (res) { /* Handler(s) failed to reload */ bbs_dprintf(fd, "%s\n", name ? "Reload failed" : "Full or partial reload failure"); diff --git a/configs/lbbs.service b/configs/lbbs.service new file mode 100755 index 00000000..34d97ffb --- /dev/null +++ b/configs/lbbs.service @@ -0,0 +1,53 @@ +# LBBS service file +# If you use this, it's recommended you add require = mod_systemd.so to /etc/lbbs/modules.conf + +[Unit] +Description=LBBS bulletin board system daemon +After=network.target + +[Service] +# If you have a newer version of systemd (>= 253, as reported by systemctl --version) +# then you can use the newer 'notify-reload' type and 'ReloadSignal'. +# Otherwise, use 'notify' and 'ExecReload' for compatibility. +#Type=notify-reload +Type=notify + +# SIGHUP is ignored since remote console disconnects trigger it. +# Therefore, we pick another unused signal to use for reload. +# SIGTERM is implicitly used for shutdown so it's not specified here. +#ReloadSignal=SIGUSR2 + +NotifyAccess=main +Environment=HOME=/home/bbs +WorkingDirectory=/home/bbs +User=bbs +Group=bbs + +# Only one of these two prestart commands is needed. +# The first one doesn't seem to work as reliably, but the latter is less secure. +#ExecStartPre=+setcap CAP_NET_BIND_SERVICE=+eip /usr/sbin/lbbs +ExecStartPre=+sysctl net.ipv4.ip_unprivileged_port_start=18 + +# Since ExecStart will start with the BBS user's privileges (not root), +# tasks that the BBS normally does prior to dropping privileges, +# if started as root, won't succeed. Thus, we do them beforehand. +ExecStartPre=+install -d -m 0755 -o bbs -g bbs /var/run/lbbs +ExecStartPre=+install -d -m 0744 -o bbs -g bbs /var/log/lbbs + +ExecStart=/usr/sbin/lbbs -gcb + +# Only needed if Type is notify, not needed if notify-reload +ExecReload=kill -USR2 $MAINPID + +LimitCORE=infinity +Restart=on-failure +RestartSec=5 +PrivateTmp=false + +# Since logs are already saved to disk, there's normally no need for output +# For debugging (e.g. on startup failure), it may be helpful to remove this +# (to enable logging by systemd) +StandardOutput=null + +[Install] +WantedBy=multi-user.target diff --git a/configs/modules.conf b/configs/modules.conf index 6de1dd58..5d80ce25 100644 --- a/configs/modules.conf +++ b/configs/modules.conf @@ -24,6 +24,9 @@ load = io_compress.so load = mod_lmdb.so load = mod_mysql.so ; mod_auth_mysql.so and mod_chanserv.so depend on this module +; systemd support - Only load if your system supports systemd. If you want to use it, it's recommended you require it +;require = mod_systemd.so + ; Basic doors load = door_usermgmt.so load = door_tutorial.so diff --git a/include/event.h b/include/event.h index 9f2ab0fc..1e36c500 100644 --- a/include/event.h +++ b/include/event.h @@ -14,8 +14,9 @@ */ enum bbs_event_type { - EVENT_STARTUP = 0, - EVENT_SHUTDOWN, + EVENT_STARTUP = 0, /*!< BBS is fully started */ + EVENT_SHUTDOWN, /*!< BBS is going to shut down */ + EVENT_RELOAD, /*!< BBS reloaded configuration */ EVENT_NODE_SHORT_SESSION, /*!< Extremely short node session (where abnormal) */ EVENT_NODE_ENCRYPTION_FAILED, /*!< TLS setup failed */ EVENT_NODE_LOGIN_FAILED, /*!< Authentication failed */ diff --git a/include/reload.h b/include/reload.h index 409a882d..e879bfdf 100644 --- a/include/reload.h +++ b/include/reload.h @@ -23,3 +23,11 @@ * Reload handlers are automatically unregistered only at shutdown. */ int bbs_register_reload_handler(const char *name, const char *description, int (*reloader)(int fd)); + +/*! + * \brief Execute reload handler(s) + * \param name Handler to execute. If NULL, all handlers will be executed. + * \param fd File descriptor for output messages from handlers, -1 to discard + * \retval 0 on success, -1 if no handlers could be executed, 1 if any handlers returned nonzero + */ +int bbs_reload(const char *name, int fd); diff --git a/modules/Makefile b/modules/Makefile index f84df12c..3d42f51e 100644 --- a/modules/Makefile +++ b/modules/Makefile @@ -13,7 +13,7 @@ GMIME_FLAGS := $(filter-out -I/usr/include/libassuan2%,$(GMIME_FLAGS)) GMIME_LIBS=$(shell pkg-config --libs glib-2.0 gmime-3.0) MYSQL_LIBS := $(shell mysql_config --libs) -ALPINE_LINUX := $(shell ls /etc/alpine-release | wc -l) +ALPINE_LINUX := $(shell ls /etc/alpine-release 2>/dev/null | wc -l) MOD_WEBMAIL_LIBS := -ljansson -lssl -lcrypto ifneq ($(ALPINE_LINUX),1) MOD_WEBMAIL_LIBS += -ldb-5.3 @@ -125,6 +125,10 @@ mod_smtp_filter_spf.so : mod_smtp_filter_spf.o @echo " [LD] $^ -> $@" $(CC) -shared -fPIC -o $(basename $^).so $^ -lspf2 +mod_systemd.so : mod_systemd.o + @echo " [LD] $^ -> $@" + $(CC) -shared -fPIC -o $(basename $^).so $^ -lsystemd + mod_webmail.so : mod_webmail.o @echo " [LD] $^ -> $@" $(CC) -shared -fPIC -o $(basename $^).so $^ $(MOD_WEBMAIL_LIBS) -L/usr/local/lib -Wl,-rpath=/usr/local/lib/ -letpan diff --git a/modules/mod_systemd.c b/modules/mod_systemd.c new file mode 100755 index 00000000..32668490 --- /dev/null +++ b/modules/mod_systemd.c @@ -0,0 +1,55 @@ +/* + * LBBS -- The Lightweight Bulletin Board System + * + * Copyright (C) 2025, Naveen Albert + * + * Naveen Albert + * + * This program is free software, distributed under the terms of + * the GNU General Public License Version 2. See the LICENSE file + * at the top of the source tree. + */ + +/*! \file + * + * \brief systemd signal support + * + * \author Naveen Albert + */ + +#include "include/bbs.h" + +#include + +#include "include/module.h" +#include "include/event.h" + +static int event_cb(struct bbs_event *event) +{ + switch (event->type) { + case EVENT_STARTUP: + sd_notifyf(0, "READY=1\nMAINPID=%lu", (unsigned long) getpid()); + break; + case EVENT_SHUTDOWN: + sd_notify(0, "STOPPING=1"); + break; + case EVENT_RELOAD: + sd_notify(0, "READY=1"); + break; + default: + return 0; + } + return 1; +} + +static int load_module(void) +{ + return bbs_register_event_consumer(event_cb); +} + +static int unload_module(void) +{ + return bbs_unregister_event_consumer(event_cb); +} + +BBS_MODULE_INFO_STANDARD("systemd support"); diff --git a/scripts/install_prereq.sh b/scripts/install_prereq.sh index 053b7d18..196a024f 100755 --- a/scripts/install_prereq.sh +++ b/scripts/install_prereq.sh @@ -172,6 +172,14 @@ PACKAGES_DEBIAN="$PACKAGES_DEBIAN libsieve2-dev" # MISSING: RPM package # MISSING: Arch package +# mod_systemd +PACKAGES_DEBIAN="$PACKAGES_DEBIAN libsystemd-dev" +PACKAGES_FEDORA="$PACKAGES_FEDORA systemd-devel" +PACKAGES_RHEL="$PACKAGES_RHEL systemd-devel" +PACKAGES_ARCH="$PACKAGES_ARCH systemd-libs" +# Alpine Linux doesn't use systemd, so no need for that here! +# No systemd on FreeBSD, or other Unices + # Soft dependencies # used for bc (executed by 'calc' in door_utils) PACKAGES_DEBIAN="$PACKAGES_DEBIAN bc"