Skip to content

Commit

Permalink
mod_systemd: Add systemd support.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
InterLinked1 committed Feb 18, 2025
1 parent 0ea87e3 commit 5c5c52b
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 24 deletions.
16 changes: 16 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down Expand Up @@ -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?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
71 changes: 51 additions & 20 deletions bbs/bbs.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
#include <sys/ioctl.h>

#include "include/module.h" /* use load_modules */
#include "include/reload.h"
#include "include/alertpipe.h"
#include "include/os.h"
#include "include/system.h"
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand All @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions bbs/event.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 18 additions & 1 deletion bbs/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down
53 changes: 53 additions & 0 deletions configs/lbbs.service
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions configs/modules.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions include/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
8 changes: 8 additions & 0 deletions include/reload.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
6 changes: 5 additions & 1 deletion modules/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 5c5c52b

Please sign in to comment.