In an earlier post, I showed how to log every shell command to a remote server using PROMPT_COMMAND
in bash
. It’s a neat trick, especially if you’re interested in session tracking, auditing, or just being a responsible sysadmin. But if you’re using BusyBox as your shell, and a lot of embedded systems and network devices do, you’ll quickly find out that this trick doesn’t work at all. So, what gives?
Bash Has PROMPT_COMMAND
, BusyBox Doesn’t
If you’re used to bash
, you know PROMPT_COMMAND
is a handy little feature: before each command runs, the shell will execute whatever you put in that variable. Perfect spot for a logging hook.
But BusyBox’s shell applet, ash
, doesn’t have PROMPT_COMMAND
. Not even a secret version of it hiding somewhere. So if you try to replicate this kind of behavior in BusyBox, the shell just stares blankly at you and does… nothing.
This isn’t a bug or a missing package. BusyBox ash
is designed to be small, fast, and simple; which means it skips the bells and whistles you’d find in a full shell like bash
.
But Wait... Is This Really Useful?
Yes, actually. This isn’t just a case of “because I can.”
Think about it: a lot of network gear these days runs Linux under the hood: firewalls, routers, switches, access points, you name it. Many of them use BusyBox because it’s lightweight and gets the job done.
But in those environments, auditing is not optional. People managing these devices often come from networking backgrounds where things like TACACS+ command logging have been standard for decades. They expect that every command typed into a shell is logged somewhere, preferably off the device.
So this isn’t just a fun weekend hack. It’s a small but important feature that brings BusyBox a little closer to what these environments need.
A Peek Under the Hood: Why getenv()
Doesn’t Work
Let’s say you want to control this feature using environment variables, like this:
export LOG_RHOST=10.0.0.1 export LOG_RPORT=5555 export SESSIONID_=abc123
Then, in your shell code, you try to do something like:
const char *host = getenv("LOG_RHOST");
And you get… NULL
.
Why? Well, here’s the fun part: BusyBox ash
keeps its own private stash of environment variables, separate from the system-wide environment you get with getenv()
. It only syncs them up when launching child processes... not inside the shell itself. At least, this is what I read... Somewhere in the code or maybe in a commit message... But if I need to say, I didn't experience this myself.
So the variable is set, export
ed, and even shows up when you run set
, but your getenv()
call is talking to the wrong guy.
To fix this, you need to use BusyBox’s internal API:
const char *host = lookupvar("LOG_RHOST");
This asks the shell directly, and it gives you the right answer.
Lesson learned: if you’re inside BusyBox ash
, use the tools it gives you, not the standard C library ones.
The Trick: Dependency Injection (Just Like the Cool Parts of BusyBox)
Here’s what I did:
Define a function pointer type in libbb
:
typedef const char* (*injected_var_lookup_t)(const char *name);
Declare a static variable to hold the injected function:
static injected_var_lookup_t injected_lookup_var;
Add a public setter function to libbb
:
void injected_set_var_lookup(injected_var_lookup_t func) { injected_lookup_var = func; }
In ash.c
, during initialization, pass in the actual lookupvar()
function: Now, anywhere in libbb
, I can safely call:
const char *val = injected_lookup_var("LOG_RHOST");
And it’ll work because, at runtime, it’s calling the real lookupvar() function from ash. This pattern keeps the code clean, avoids nasty cross-module hacks, and is totally in line with how BusyBox handles similar internal wiring elsewhere. I thought I was a genius for inventing this, until I realized BusyBox already did the same thing in math.c
. So… maybe I’m just a very good copy-paste engineer.
What This Patch Adds
So I wrote a patch for BusyBox that brings this remote command logging feature to life. Here’s what it does:
- Watches each command entered in the shell.
- If the logging environment variables are set (
LOG_RHOST
,LOG_RPORT
,SESSIONID_
), it sends the command over TCP to the remote server. - It includes the session ID in the log line, so you can trace which session ran what.
That’s it. Lightweight, optional, and it doesn’t get in the way if you’re not using it.
The Patch
Here’s the code that makes it happen: it applies nicely to busybox-1.37.0
From 5b663cd894f6418673686290248fe8776af2434d Mon Sep 17 00:00:00 2001 From: Alessandro Carminati <alessandro.carminati@gmail.com> Date: Fri, 27 Jun 2025 14:05:21 +0200 Subject: [PATCH] ash: add support for logging executed commands to a remote server Content-type: text/plain This commit adds functionality to the ash shell that sends each executed command to a remote logging server over TCP, enabling remote auditing and session tracking. The design is inspired by the tacacs2 approach used in network devices. This is particularly useful in embedded Linux environments replacing traditional routers, where audit trails are essential. Unlike bash, ash does not support PROMPT_COMMAND. This implementation fills that gap using internal hooks in the shell. The feature is controlled via three environment variables: - SESSIONID_ : unique identifier for the shell session - LOG_RHOST : remote log server hostname or IP address - LOG_RPORT : remote log server TCP port When these variables are set, each command entered is sent to the specified logging server, prepended with the session ID. This enhancement is lightweight and optional, and does not impact users who do not configure the environment variables Signed-off-by: Alessandro Carminati <acarmina@redhat.com> --- include/libbb.h | 7 +++ libbb/Config.src | 10 ++++ libbb/Kbuild.src | 1 + libbb/lineedit.c | 3 ++ libbb/loggers_utils.c | 114 ++++++++++++++++++++++++++++++++++++++++++ shell/ash.c | 3 ++ 6 files changed, 138 insertions(+) create mode 100644 libbb/loggers_utils.c diff --git a/include/libbb.h b/include/libbb.h index 01cdb1b..870b9f5 100644 --- a/include/libbb.h +++ b/include/libbb.h @@ -2003,6 +2003,9 @@ void free_line_input_t(line_input_t *n) FAST_FUNC; #else # define free_line_input_t(n) free(n) #endif +# if ENABLE_FEATURE_SEND_COMMAND_REMOTE +void loggers_utils_set_var_lookup(void *func); +# endif /* * maxsize must be >= 2. * Returns: @@ -2133,6 +2136,10 @@ enum { PSSCAN_RUIDGID = (1 << 21) * ENABLE_FEATURE_PS_ADDITIONAL_COLUMNS, PSSCAN_TASKS = (1 << 22) * ENABLE_FEATURE_SHOW_THREADS, }; +# if ENABLE_FEATURE_SEND_COMMAND_REMOTE +int rlog_this(const char *history_itm); +# endif + //procps_status_t* alloc_procps_scan(void) FAST_FUNC; void free_procps_scan(procps_status_t* sp) FAST_FUNC; procps_status_t* procps_scan(procps_status_t* sp, int flags) FAST_FUNC; diff --git a/libbb/Config.src b/libbb/Config.src index b980f19..a6f5882 100644 --- a/libbb/Config.src +++ b/libbb/Config.src @@ -202,6 +202,16 @@ config FEATURE_EDITING_SAVE_ON_EXIT help Save history on shell exit, not after every command. +config FEATURE_SEND_COMMAND_REMOTE + bool "Send last command to remote logger for audit" + default n + depends on FEATURE_EDITING_SAVEHISTORY + help + Send last command to remote logger for audit. + It is mandatory that LOG_RHOST and LOG_RPORT environment variables + are defined to specify the remote ip and port where send logs. + It alse needs the environment SESSIONID_ to be defined as sessionid. + config FEATURE_REVERSE_SEARCH bool "Reverse history search" default y diff --git a/libbb/Kbuild.src b/libbb/Kbuild.src index cb8d2c2..096a9f3 100644 --- a/libbb/Kbuild.src +++ b/libbb/Kbuild.src @@ -208,3 +208,4 @@ lib-$(CONFIG_FEATURE_CUT_REGEX) += xregcomp.o # Add the experimental logging functionality, only used by zcip lib-$(CONFIG_ZCIP) += logenv.o +lib-$(CONFIG_FEATURE_SEND_COMMAND_REMOTE) += loggers_utils.o diff --git a/libbb/lineedit.c b/libbb/lineedit.c index 543a3f1..8140f00 100644 --- a/libbb/lineedit.c +++ b/libbb/lineedit.c @@ -1685,6 +1685,9 @@ static void remember_in_history(char *str) /* i <= state->max_history-1 */ state->history[i++] = xstrdup(str); /* i <= state->max_history */ +# if ENABLE_FEATURE_SEND_COMMAND_REMOTE + rlog_this(state->history[i-1]); +# endif state->cur_history = i; state->cnt_history = i; # if ENABLE_FEATURE_EDITING_SAVEHISTORY && !ENABLE_FEATURE_EDITING_SAVE_ON_EXIT diff --git a/libbb/loggers_utils.c b/libbb/loggers_utils.c new file mode 100644 index 0000000..d1266e8 --- /dev/null +++ b/libbb/loggers_utils.c @@ -0,0 +1,114 @@ +/* + * This code allows remote logging of the commands. + * + * Copyright (c) 2025 Alessandro Carminati <acarmina@redhat.com> + * + * Licensed under GPLv2 or later, see file LICENSE in this source tree. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <netdb.h> +#include <sys/socket.h> + +#define SESSION_ID_ENV "SESSIONID_" +#define SESSION_LEN 9 +#define SESSION_RHOST "LOG_RHOST" +#define SESSION_RPORT "LOG_RPORT" + +typedef const char* (*loggers_utils_var_lookup_t)(const char *name); + +void get_timestamp(char *, size_t); +int send_log(const char *, const char *, const char *); +int rlog_this(const char *); +void loggers_utils_set_var_lookup(void *func); + +static loggers_utils_var_lookup_t loggers_utils_lookup_var; + +void loggers_utils_set_var_lookup(void *func) { + loggers_utils_lookup_var = (loggers_utils_var_lookup_t) func; +} + +void get_timestamp(char *buf, size_t len) { + time_t now = time(NULL); + struct tm *tm_info = localtime(&now); + strftime(buf, len, "%Y%m%d.%H%M%S", tm_info); +} + +int send_log(const char *line, const char *host, const char *port_str) { + int sockfd; + struct addrinfo hints, *res, *p; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + if (getaddrinfo(host, port_str, &hints, &res) != 0) { + fprintf(stderr, "send_log: cant' resolve host in %s\n", + SESSION_RHOST); + return -1; + } + + for (p = res; p != NULL; p = p->ai_next) { + sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (sockfd < 0) continue; + if (connect(sockfd, p->ai_addr, p->ai_addrlen) == 0) break; + close(sockfd); + } + + if (p == NULL) { + fprintf(stderr, "send_log: Unable to connect to %s:%s\n", + host, port_str); + freeaddrinfo(res); + return -1; + } + + ssize_t len = strlen(line); + if (send(sockfd, line, len, 0) != len) { + fprintf(stderr, "send_log: Unable to send data\n"); + close(sockfd); + freeaddrinfo(res); + return -1; + } + + close(sockfd); + freeaddrinfo(res); + return 0; +} + +int rlog_this(const char *history_itm) { + char timestamp[32], hostname[64]; + char *sess_id, *r_ip, *r_port; + char logline[1500]; + + if (!loggers_utils_lookup_var) return -1; + + sess_id = loggers_utils_lookup_var(SESSION_ID_ENV); + if (!sess_id || (strlen(sess_id) > 9)) return 1; + + r_ip = loggers_utils_lookup_var(SESSION_RHOST); + if (!r_ip) return -1; + + r_port = loggers_utils_lookup_var(SESSION_RPORT); + if (!r_port) return -1; + + if (!atoi(r_port)) return -1; + + get_timestamp(timestamp, sizeof(timestamp)); + gethostname(hostname, sizeof(hostname)); + + snprintf(logline, sizeof(logline), "%s - %s - %s > %s\n", + timestamp, sess_id, hostname, history_itm); + + if (send_log(logline, r_ip, r_port)!=0){ + fprintf(stderr, "rlog_this: can't send log to remote.\n"); + return -2; + }; + + return 0; +} diff --git a/shell/ash.c b/shell/ash.c index bbd7307..e021def 100644 --- a/shell/ash.c +++ b/shell/ash.c @@ -9780,6 +9780,9 @@ setinteractive(int on) did_banner = 1; } #endif +# if ENABLE_FEATURE_SEND_COMMAND_REMOTE + loggers_utils_set_var_lookup(&lookupvar); +# endif #if ENABLE_FEATURE_EDITING if (!line_input_state) { line_input_state = new_line_input_t(FOR_SHELL | WITH_PATH_LOOKUP); -- 2.25.1