Files
heartbeat/scripts/c/hbc_mini.c
T
Andreas WredeandClaude Opus 4.8 ae3f2fc70f feat: gate remote command execution behind allow_remote_command
CMD packets arrive as unauthenticated UDP datagrams, yet every hbc client
executed the shell command they carry without any opt-in. Add an
allow_remote_command config key, default false: when off, the command is
logged and refused with "Refused: allow_remote_command is false" (visible in
the server event log under the command service), and subprocess is never
reached. When on, the client warns at startup that it will execute CMD
packets.

Applied to all four clients that handle CMD — hbc, hbc_windows.py,
hbc_mini.py, and the C hbc_mini — since gating only one leaves the others
wide open. The C client reads the same key from ~/.hbc.json and needs a
rebuild to pick it up. UPD (self-update) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:10:41 -07:00

1457 lines
53 KiB
C

/* hbc_mini.c — HeartBeat Client, single-file C port of hbc_mini.py
*
* Build: cc -O2 -o hbc_mini hbc_mini.c -lz -lpthread -lm
* Requires: zlib, pthreads, POSIX
* Supported: Linux, FreeBSD, NetBSD, DragonFly BSD
*
* Drop on any host and run: ./hbc_mini <hbd-host>
* Config: ~/.hbc.json (same keys as Python version)
*/
#ifdef __linux__
# define _GNU_SOURCE
# define _POSIX_C_SOURCE 200809L
#endif
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <netdb.h>
#include <pthread.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <zlib.h>
#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
# include <sys/sysctl.h>
# include <net/if.h>
# include <net/if_dl.h>
# include <ifaddrs.h>
#endif
#if defined(__NetBSD__)
# include <uvm/uvm_extern.h>
#endif
/* version updated by scripts/bumpminor.sh */
#define HBC_VERSION "5.2.4"
#define DEFAULT_PORT 50003
#define DEFAULT_INTERVAL 10
#define MAX_HOSTS 8
#define MAX_KV 64
#define MAX_KEY 128
#define MAX_VAL 4096
#define PROTO_BUF_SIZE 65536
#define MAX_PING_HOSTS 32
#define MAX_NAGIOS_CMDS 32
#define MAX_DISK_MOUNTS 32
#define MAX_NET_SKIP 16
/* ============================================================
* Logging
* ============================================================ */
static bool g_use_syslog = false;
static int g_log_level = 3; /* 0=debug 1=info 2=warn 3=error */
#define LL_DEBUG 0
#define LL_INFO 1
#define LL_WARN 2
#define LL_ERROR 3
static void hbc_log(int level, const char *fmt, ...) {
if (level < g_log_level) return;
va_list ap;
va_start(ap, fmt);
if (g_use_syslog) {
int pri = LOG_ERR;
if (level == LL_DEBUG) pri = LOG_DEBUG;
else if (level == LL_INFO) pri = LOG_INFO;
else if (level == LL_WARN) pri = LOG_WARNING;
vsyslog(pri, fmt, ap);
} else {
const char *ln[] = {"DEBUG","INFO","WARN","ERROR"};
time_t t = time(NULL);
struct tm tm; localtime_r(&t, &tm);
char ts[32]; strftime(ts, sizeof(ts), "%Y-%m-%d %H:%M:%S", &tm);
fprintf(stderr, "%s hbc %s: ", ts, ln[level]);
vfprintf(stderr, fmt, ap);
fputc('\n', stderr);
}
va_end(ap);
}
#define LOGD(fmt, ...) hbc_log(LL_DEBUG, fmt, ##__VA_ARGS__)
#define LOGI(fmt, ...) hbc_log(LL_INFO, fmt, ##__VA_ARGS__)
#define LOGW(fmt, ...) hbc_log(LL_WARN, fmt, ##__VA_ARGS__)
#define LOGE(fmt, ...) hbc_log(LL_ERROR, fmt, ##__VA_ARGS__)
/* ============================================================
* Key-Value dict (protocol messages)
* ============================================================ */
typedef struct { char key[MAX_KEY]; char val[MAX_VAL]; } kv_t;
typedef struct { kv_t pairs[MAX_KV]; int count; } kvdict_t;
static void kv_clear(kvdict_t *d) { d->count = 0; }
static bool kv_set(kvdict_t *d, const char *key, const char *val) {
if (d->count >= MAX_KV) return false;
snprintf(d->pairs[d->count].key, MAX_KEY, "%s", key);
snprintf(d->pairs[d->count].val, MAX_VAL, "%s", val);
d->count++;
return true;
}
static bool kv_set_int(kvdict_t *d, const char *key, long long v) {
char buf[32]; snprintf(buf, sizeof(buf), "%lld", v);
return kv_set(d, key, buf);
}
static bool kv_set_ull(kvdict_t *d, const char *key, unsigned long long v) {
char buf[32]; snprintf(buf, sizeof(buf), "%llu", v);
return kv_set(d, key, buf);
}
static bool kv_set_dbl(kvdict_t *d, const char *key, double v) {
char buf[32];
if (isinf(v)) snprintf(buf, sizeof(buf), "inf");
else snprintf(buf, sizeof(buf), "%.5f", v);
return kv_set(d, key, buf);
}
static const char *kv_get(const kvdict_t *d, const char *key) {
for (int i = 0; i < d->count; i++)
if (strcmp(d->pairs[i].key, key) == 0)
return d->pairs[i].val;
return NULL;
}
/* ============================================================
* Protocol codec (mirrors hbd/common/proto.py)
* ============================================================ */
/* Build wire bytes: !MSG: + zlib-compressed key=val;key=val;... */
static int proto_encode(const char *msg_id, const kvdict_t *d,
uint8_t *out, size_t cap) {
/* Build payload */
char *payload = malloc(PROTO_BUF_SIZE);
if (!payload) return -1;
int plen = 0;
for (int i = 0; i < d->count; i++) {
if (i > 0 && plen < PROTO_BUF_SIZE - 1) payload[plen++] = ';';
plen += snprintf(payload + plen, PROTO_BUF_SIZE - plen,
"%s=%s", d->pairs[i].key, d->pairs[i].val);
if (plen >= PROTO_BUF_SIZE) { plen = PROTO_BUF_SIZE - 1; break; }
}
/* Compress */
uLongf clen = compressBound(plen);
uint8_t *cbuf = malloc(clen);
if (!cbuf) { free(payload); return -1; }
if (compress2(cbuf, &clen, (uint8_t *)payload, plen, 6) != Z_OK) {
free(payload); free(cbuf); return -1;
}
free(payload);
/* Header: !XXX: (5 bytes) */
if ((size_t)(5 + clen) > cap) { free(cbuf); return -1; }
out[0] = '!';
memcpy(out + 1, msg_id, 3);
out[4] = ':';
memcpy(out + 5, cbuf, clen);
free(cbuf);
return 5 + (int)clen;
}
/* Parse wire bytes into kvdict */
static bool proto_decode(const uint8_t *data, int dlen, kvdict_t *out) {
kv_clear(out);
if (dlen < 5 || data[0] != '!') return false;
char id[4] = {0};
memcpy(id, data + 1, 3);
kv_set(out, "ID", id);
uint8_t *payload = malloc(PROTO_BUF_SIZE);
if (!payload) return false;
uLongf plen = PROTO_BUF_SIZE - 1;
if (uncompress(payload, &plen, data + 5, dlen - 5) != Z_OK) {
free(payload); return false;
}
payload[plen] = '\0';
char *p = (char *)payload;
while (*p) {
char *semi = strchr(p, ';');
if (semi) *semi = '\0';
char *eq = strchr(p, '=');
if (eq) {
*eq = '\0';
char *kp = p; while (*kp == ' ') kp++;
char *vp = eq+1; while (*vp == ' ') vp++;
kv_set(out, kp, vp);
}
if (semi) p = semi + 1; else break;
}
free(payload);
return true;
}
/* ============================================================
* Minimal JSON parser (for config file)
* ============================================================ */
typedef enum { JT_NULL,JT_BOOL,JT_INT,JT_FLOAT,JT_STRING,JT_ARRAY,JT_OBJECT } jtype_t;
typedef struct jval jval_t;
struct jval {
jtype_t type;
union {
bool b;
long long i;
double f;
char *s;
struct { char **keys; jval_t **vals; int count, cap; } obj;
struct { jval_t **items; int count, cap; } arr;
};
};
static void jval_free(jval_t *v) {
if (!v) return;
if (v->type == JT_STRING) { free(v->s); }
else if (v->type == JT_OBJECT) {
for (int i = 0; i < v->obj.count; i++) { free(v->obj.keys[i]); jval_free(v->obj.vals[i]); }
free(v->obj.keys); free(v->obj.vals);
} else if (v->type == JT_ARRAY) {
for (int i = 0; i < v->arr.count; i++) jval_free(v->arr.items[i]);
free(v->arr.items);
}
free(v);
}
static const char *jskip(const char *p) {
while (*p && isspace((unsigned char)*p)) p++;
return p;
}
static const char *jparse_value(const char *p, jval_t **out);
static const char *jparse_string(const char *p, char **out) {
if (*p != '"') return NULL;
p++;
char buf[4096]; int n = 0;
while (*p && *p != '"') {
if (*p == '\\') {
p++;
switch (*p) {
case '"': buf[n++]='"'; break; case '\\': buf[n++]='\\'; break;
case 'n': buf[n++]='\n'; break; case 'r': buf[n++]='\r'; break;
case 't': buf[n++]='\t'; break; default: buf[n++]=*p; break;
}
} else { buf[n++] = *p; }
p++;
if (n >= (int)sizeof(buf)-1) break;
}
buf[n] = '\0';
if (*p == '"') p++;
*out = strdup(buf);
return p;
}
static const char *jparse_object(const char *p, jval_t **out) {
if (*p != '{') return NULL;
p++;
jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_OBJECT;
p = jskip(p);
while (*p && *p != '}') {
p = jskip(p);
char *key = NULL;
p = jparse_string(p, &key);
if (!p) break;
p = jskip(p); if (*p == ':') p++;
p = jskip(p);
jval_t *val = NULL;
p = jparse_value(p, &val);
if (key && val) {
if (v->obj.count >= v->obj.cap) {
v->obj.cap = v->obj.cap ? v->obj.cap*2 : 4;
v->obj.keys = realloc(v->obj.keys, v->obj.cap * sizeof(char*));
v->obj.vals = realloc(v->obj.vals, v->obj.cap * sizeof(jval_t*));
}
v->obj.keys[v->obj.count] = key;
v->obj.vals[v->obj.count] = val;
v->obj.count++;
} else { free(key); jval_free(val); }
p = jskip(p); if (*p == ',') p++;
p = jskip(p);
}
if (*p == '}') p++;
*out = v; return p;
}
static const char *jparse_array(const char *p, jval_t **out) {
if (*p != '[') return NULL;
p++;
jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_ARRAY;
p = jskip(p);
while (*p && *p != ']') {
jval_t *item = NULL;
p = jparse_value(jskip(p), &item);
if (item) {
if (v->arr.count >= v->arr.cap) {
v->arr.cap = v->arr.cap ? v->arr.cap*2 : 4;
v->arr.items = realloc(v->arr.items, v->arr.cap * sizeof(jval_t*));
}
v->arr.items[v->arr.count++] = item;
}
p = jskip(p); if (*p == ',') p++;
}
if (*p == ']') p++;
*out = v; return p;
}
static const char *jparse_value(const char *p, jval_t **out) {
p = jskip(p);
if (!*p) { *out = NULL; return p; }
if (*p == '{') return jparse_object(p, out);
if (*p == '[') return jparse_array(p, out);
if (*p == '"') {
char *s = NULL; p = jparse_string(p, &s);
jval_t *v = calloc(1, sizeof(jval_t)); v->type = JT_STRING; v->s = s;
*out = v; return p;
}
if (strncmp(p,"true",4)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_BOOL; v->b=true; *out=v; return p+4; }
if (strncmp(p,"false",5)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_BOOL; v->b=false; *out=v; return p+5; }
if (strncmp(p,"null",4)==0) { jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_NULL; *out=v; return p+4; }
if (isdigit((unsigned char)*p) || *p == '-') {
char *e1, *e2;
long long iv = strtoll(p, &e1, 10);
double fv = strtod(p, &e2);
if (e2 > e1) {
jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_FLOAT; v->f=fv; *out=v; return e2;
} else {
jval_t *v=calloc(1,sizeof(jval_t)); v->type=JT_INT; v->i=iv; *out=v; return e1;
}
}
*out = NULL; return p + 1;
}
static jval_t *json_parse(const char *src) {
jval_t *v = NULL; jparse_value(src, &v); return v;
}
static jval_t *jget(const jval_t *obj, const char *key) {
if (!obj || obj->type != JT_OBJECT) return NULL;
for (int i = 0; i < obj->obj.count; i++)
if (strcmp(obj->obj.keys[i], key) == 0) return obj->obj.vals[i];
return NULL;
}
static long long jint(const jval_t *v, long long def) {
if (!v) return def;
if (v->type==JT_INT) return v->i;
if (v->type==JT_FLOAT) return (long long)v->f;
if (v->type==JT_BOOL) return v->b ? 1 : 0;
return def;
}
static const char *jstr(const jval_t *v, const char *def) {
return (!v || v->type!=JT_STRING) ? def : v->s;
}
/* ============================================================
* Config
* ============================================================ */
/* Set from config in cfg_load. Off by default: a CMD packet is an unauthenticated
* UDP datagram, so executing one must be opted into per host. */
static bool g_allow_remote_command = false;
typedef struct {
int hb_port, interval;
char owner[256];
char ping_hosts[MAX_PING_HOSTS][256];
int ping_host_count, ping_interval, ping_count, ping_timeout;
struct { char name[64]; char cmd[512]; } nagios[MAX_NAGIOS_CMDS];
int nagios_count, nagios_interval, nagios_timeout;
int cpu_interval, mem_interval, disk_interval, net_interval;
char disk_mounts[MAX_DISK_MOUNTS][256];
int disk_mount_count;
char net_skip[MAX_NET_SKIP][64];
int net_skip_count;
} config_t;
static void config_defaults(config_t *c) {
memset(c, 0, sizeof(*c));
c->hb_port = DEFAULT_PORT;
c->interval = DEFAULT_INTERVAL;
c->ping_interval = 60; c->ping_count = 3; c->ping_timeout = 5;
c->nagios_interval = 300; c->nagios_timeout = 30;
c->cpu_interval = 300;
c->mem_interval = 300;
c->disk_interval = 300;
c->net_interval = 300;
snprintf(c->net_skip[0], 64, "lo");
c->net_skip_count = 1;
}
static void config_load(config_t *cfg, const char *path) {
config_defaults(cfg);
char fpath[512];
if (!path) {
const char *home = getenv("HOME");
snprintf(fpath, sizeof(fpath), "%s/.hbc.json", home ? home : "");
path = fpath;
}
FILE *f = fopen(path, "r");
if (!f) return;
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
char *buf = malloc(sz + 1);
if (!buf) { fclose(f); return; }
size_t _nr = fread(buf, 1, sz, f); buf[_nr] = '\0'; fclose(f);
jval_t *root = json_parse(buf); free(buf);
if (!root || root->type != JT_OBJECT) { LOGW("cannot parse %s", path); jval_free(root); return; }
jval_t *v;
if ((v = jget(root, "hb_port"))) cfg->hb_port = jint(v, cfg->hb_port);
if ((v = jget(root, "interval"))) cfg->interval = jint(v, cfg->interval);
if ((v = jget(root, "owner"))) snprintf(cfg->owner, sizeof(cfg->owner), "%s", jstr(v, ""));
if ((v = jget(root, "allow_remote_command"))) g_allow_remote_command = jint(v, 0) != 0;
if (g_allow_remote_command)
LOGI("allow_remote_command is true - CMD packets from the server will be executed");
jval_t *plugins = jget(root, "plugins");
/* ping_monitor */
jval_t *pm = plugins ? jget(plugins,"ping_monitor") : jget(root,"ping_monitor");
if (pm && pm->type == JT_OBJECT) {
if ((v=jget(pm,"interval"))) cfg->ping_interval = jint(v, cfg->ping_interval);
if ((v=jget(pm,"count"))) cfg->ping_count = jint(v, cfg->ping_count);
if ((v=jget(pm,"timeout"))) cfg->ping_timeout = jint(v, cfg->ping_timeout);
jval_t *h = jget(pm, "hosts");
if (h && h->type == JT_ARRAY) {
for (int i = 0; i < h->arr.count && cfg->ping_host_count < MAX_PING_HOSTS; i++) {
const char *s = jstr(h->arr.items[i], NULL);
if (s) snprintf(cfg->ping_hosts[cfg->ping_host_count++], 256, "%s", s);
}
} else if (h && h->type == JT_OBJECT) {
for (int i = 0; i < h->obj.count && cfg->ping_host_count < MAX_PING_HOSTS; i++)
snprintf(cfg->ping_hosts[cfg->ping_host_count++], 256, "%s", h->obj.keys[i]);
}
}
/* nagios_runner */
jval_t *nr = plugins ? jget(plugins,"nagios_runner") : jget(root,"nagios_runner");
if (nr && nr->type == JT_OBJECT) {
if ((v=jget(nr,"interval"))) cfg->nagios_interval = jint(v, cfg->nagios_interval);
if ((v=jget(nr,"timeout"))) cfg->nagios_timeout = jint(v, cfg->nagios_timeout);
jval_t *cmds = jget(nr, "commands");
if (cmds && cmds->type == JT_ARRAY) {
for (int i = 0; i < cmds->arr.count && cfg->nagios_count < MAX_NAGIOS_CMDS; i++) {
jval_t *c = cmds->arr.items[i];
if (!c || c->type != JT_OBJECT) continue;
jval_t *n = jget(c,"name"), *cmd = jget(c,"command");
if (n && cmd) {
int idx = cfg->nagios_count++;
snprintf(cfg->nagios[idx].name, 64, "%s", jstr(n, ""));
snprintf(cfg->nagios[idx].cmd, 512, "%s", jstr(cmd, ""));
}
}
}
}
/* cpu/memory/disk/network */
jval_t *cpu = plugins ? jget(plugins,"cpu_monitor") : jget(root,"cpu_monitor");
if (cpu && (v=jget(cpu,"interval"))) cfg->cpu_interval = jint(v, cfg->cpu_interval);
jval_t *mem = plugins ? jget(plugins,"memory_monitor") : jget(root,"memory_monitor");
if (mem && (v=jget(mem,"interval"))) cfg->mem_interval = jint(v, cfg->mem_interval);
jval_t *disk = plugins ? jget(plugins,"disk_monitor") : jget(root,"disk_monitor");
if (disk && disk->type == JT_OBJECT) {
if ((v=jget(disk,"interval"))) cfg->disk_interval = jint(v, cfg->disk_interval);
jval_t *m = jget(disk,"mounts");
if (m && m->type == JT_ARRAY)
for (int i = 0; i < m->arr.count && cfg->disk_mount_count < MAX_DISK_MOUNTS; i++) {
const char *s = jstr(m->arr.items[i], NULL);
if (s) snprintf(cfg->disk_mounts[cfg->disk_mount_count++], 256, "%s", s);
}
}
jval_t *net = plugins ? jget(plugins,"network_monitor") : jget(root,"network_monitor");
if (net && net->type == JT_OBJECT) {
if ((v=jget(net,"interval"))) cfg->net_interval = jint(v, cfg->net_interval);
jval_t *sk = jget(net,"skip_interfaces");
if (sk && sk->type == JT_ARRAY) {
cfg->net_skip_count = 0;
for (int i = 0; i < sk->arr.count && cfg->net_skip_count < MAX_NET_SKIP; i++) {
const char *s = jstr(sk->arr.items[i], NULL);
if (s) snprintf(cfg->net_skip[cfg->net_skip_count++], 64, "%s", s);
}
}
}
LOGI("loaded config from %s", path);
jval_free(root);
}
/* ============================================================
* UDP connection
* ============================================================ */
typedef struct {
int conn_id;
char addr[INET6_ADDRSTRLEN];
int port, af, sockfd;
char name[256];
long long ack_count;
double last_send, rtt;
volatile bool request_info;
pthread_mutex_t mu;
} conn_t;
static double now_ts(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return ts.tv_sec + ts.tv_nsec * 1e-9;
}
static bool conn_open(conn_t *c) {
c->sockfd = socket(c->af, SOCK_DGRAM, 0);
if (c->sockfd < 0) { LOGE("socket: %s", strerror(errno)); return false; }
int fl = fcntl(c->sockfd, F_GETFL, 0);
fcntl(c->sockfd, F_SETFL, fl | O_NONBLOCK);
pthread_mutex_init(&c->mu, NULL);
return true;
}
static void conn_close(conn_t *c) {
if (c->sockfd >= 0) { close(c->sockfd); c->sockfd = -1; }
}
static int conn_send(conn_t *c, const char *msg_id, kvdict_t *d) {
kv_set(d, "name", c->name);
char buf[32];
snprintf(buf, sizeof(buf), "%d", c->conn_id); kv_set(d, "id", buf);
snprintf(buf, sizeof(buf), "%.3f", now_ts()); kv_set(d, "time", buf);
uint8_t wire[PROTO_BUF_SIZE];
int wlen = proto_encode(msg_id, d, wire, sizeof(wire));
if (wlen < 0) { LOGW("proto_encode failed"); return -1; }
struct sockaddr_storage sa; socklen_t salen = 0;
memset(&sa, 0, sizeof(sa));
if (c->af == AF_INET) {
struct sockaddr_in *s4 = (struct sockaddr_in *)&sa;
s4->sin_family = AF_INET;
s4->sin_port = htons(c->port);
inet_pton(AF_INET, c->addr, &s4->sin_addr);
salen = sizeof(*s4);
} else {
struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)&sa;
s6->sin6_family = AF_INET6;
s6->sin6_port = htons(c->port);
inet_pton(AF_INET6, c->addr, &s6->sin6_addr);
salen = sizeof(*s6);
}
pthread_mutex_lock(&c->mu);
c->last_send = now_ts();
ssize_t sent = sendto(c->sockfd, wire, wlen, 0, (struct sockaddr *)&sa, salen);
pthread_mutex_unlock(&c->mu);
return (sent == wlen) ? 0 : -1;
}
static void conn_recv(conn_t *c) {
uint8_t buf[PROTO_BUF_SIZE];
struct sockaddr_storage sa; socklen_t sl = sizeof(sa);
ssize_t n = recvfrom(c->sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&sa, &sl);
if (n <= 0) return;
kvdict_t msg;
if (!proto_decode(buf, (int)n, &msg)) return;
const char *id = kv_get(&msg, "ID");
if (!id) return;
if (strcmp(id, "ACK") == 0) {
c->rtt = (now_ts() - c->last_send) * 1000.0;
c->ack_count++;
const char *ru = kv_get(&msg, "request_update");
if (ru && strcmp(ru,"1") == 0) c->request_info = true;
LOGD("ACK rtt=%.1fms", c->rtt);
} else if (strcmp(id, "CMD") == 0) {
const char *cmd = kv_get(&msg, "cmd");
if (cmd && !g_allow_remote_command) {
LOGI("refused command (allow_remote_command is false): %s", cmd);
kvdict_t rep; kv_clear(&rep);
kv_set(&rep, "service", "command");
kv_set(&rep, "msg", "Refused: allow_remote_command is false");
conn_send(c, "HTB", &rep);
} else if (cmd) {
LOGI("CMD: %s", cmd);
char out[4096] = "";
FILE *p = popen(cmd, "r");
if (p) { size_t _r = fread(out, 1, sizeof(out)-1, p); (void)_r; pclose(p); }
kvdict_t rep; kv_clear(&rep);
kv_set(&rep, "service", "command");
kv_set(&rep, "msg", out[0] ? out : "OK");
conn_send(c, "HTB", &rep);
}
} else if (strcmp(id, "UPD") == 0) {
LOGI("UPD: update requested (not implemented in C version)");
}
}
/* ============================================================
* Global state
* ============================================================ */
static volatile bool g_running = true;
static volatile bool g_restart = false;
static conn_t g_conns[MAX_HOSTS];
static int g_nconns = 0;
static void sig_handler(int sig) {
if (sig == SIGHUP) g_restart = true;
g_running = false;
}
/* ============================================================
* Plugin: os_info
* ============================================================ */
static void plugin_os_info(conn_t *c, const config_t *cfg) {
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "os_info");
struct utsname u;
if (uname(&u) == 0) {
kv_set(&d, "system", u.sysname);
kv_set(&d, "node", u.nodename);
kv_set(&d, "release", u.release);
kv_set(&d, "machine", u.machine);
}
kv_set(&d, "hbc_version", HBC_VERSION);
kv_set(&d, "hbc_type", "mini-c");
if (cfg->owner[0]) kv_set(&d, "owner", cfg->owner);
#ifdef __linux__
{
FILE *f = fopen("/etc/os-release", "r");
if (!f) f = fopen("/etc/lsb-release", "r");
if (f) {
char line[512];
while (fgets(line, sizeof(line), f)) {
char *eq = strchr(line, '=');
if (!eq || line[0] == '#') continue;
*eq = '\0';
char *val = eq + 1;
int vl = strlen(val);
while (vl > 0 && (val[vl-1]=='\n'||val[vl-1]=='\r'||val[vl-1]=='"'||val[vl-1]=='\'')) val[--vl]='\0';
if (*val == '"' || *val == '\'') val++;
if (strcmp(line,"PRETTY_NAME")==0) kv_set(&d, "distro_pretty_name", val);
else if (strcmp(line,"NAME")==0) kv_set(&d, "distro_name", val);
else if (strcmp(line,"VERSION_ID")==0) kv_set(&d, "distro_version_id", val);
else if (strcmp(line,"ID")==0) kv_set(&d, "distro_id", val);
}
fclose(f);
}
}
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
{
char osver[128] = ""; size_t len = sizeof(osver);
sysctlbyname("kern.osrelease", osver, &len, NULL, 0);
if (osver[0]) kv_set(&d, "distro_version_id", osver);
}
#endif
kv_set_int(&d, "_interval", 0); /* InfoPlugin: collect-once, never stale */
conn_send(c, "PLG", &d);
LOGI("sent os_info");
}
/* ============================================================
* Plugin: cpu_monitor
* Linux: /proc/stat (user nice sys idle iowait irq softirq steal)
* BSD: sysctl kern.cp_time (CP_USER CP_NICE CP_SYS CP_INTR CP_IDLE)
* ============================================================ */
typedef struct {
long long user, sys, idle, iowait, total;
} cpu_sample_t;
#ifdef __linux__
static bool read_cpu_sample(cpu_sample_t *s) {
FILE *f = fopen("/proc/stat", "r");
if (!f) return false;
char line[256]; bool ok = false;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "cpu ", 4) != 0) continue;
long long u=0, ni=0, sy=0, id=0, iow=0, irq=0, sirq=0;
int n = sscanf(line+4, "%lld %lld %lld %lld %lld %lld %lld",
&u, &ni, &sy, &id, &iow, &irq, &sirq);
if (n < 4) break;
s->user = u + ni;
s->sys = sy + irq + sirq;
s->idle = id;
s->iowait = (n >= 5) ? iow : 0;
s->total = u + ni + sy + id + iow + irq + sirq;
ok = true; break;
}
fclose(f); return ok;
}
static void read_cpu_extras(kvdict_t *d) {
FILE *fi = fopen("/proc/cpuinfo", "r");
if (fi) {
char ln[256]; int cores = 0;
while (fgets(ln, sizeof(ln), fi)) if (strncmp(ln,"processor",9)==0) cores++;
fclose(fi);
if (cores > 0) kv_set_int(d, "cpu_core_count", cores);
}
fi = fopen("/proc/uptime", "r");
if (fi) {
double up = 0;
if (fscanf(fi, "%lf", &up) == 1) kv_set_int(d, "uptime_seconds", (long long)up);
fclose(fi);
}
}
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
static bool read_cpu_sample(cpu_sample_t *s) {
/* kern.cp_time: CP_USER(0) CP_NICE(1) CP_SYS(2) CP_INTR(3) CP_IDLE(4) */
unsigned long cp[5] = {0};
size_t len = sizeof(cp);
if (sysctlbyname("kern.cp_time", cp, &len, NULL, 0) != 0) return false;
s->user = (long long)cp[0] + (long long)cp[1];
s->sys = (long long)cp[2] + (long long)cp[3]; /* SYS + INTR */
s->idle = (long long)cp[4];
s->iowait = 0;
s->total = s->user + (long long)cp[2] + (long long)cp[3] + s->idle;
return true;
}
static void read_cpu_extras(kvdict_t *d) {
int ncpu = 0; size_t len = sizeof(ncpu);
if (sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0) == 0 && ncpu > 0)
kv_set_int(d, "cpu_core_count", ncpu);
struct timeval bt; len = sizeof(bt);
if (sysctlbyname("kern.boottime", &bt, &len, NULL, 0) == 0)
kv_set_int(d, "uptime_seconds", (long long)(time(NULL) - bt.tv_sec));
}
#endif /* platform cpu */
static cpu_sample_t g_cpu_prev;
static bool g_cpu_have_prev = false;
static void plugin_cpu_monitor(conn_t *c, const config_t *cfg) {
(void)cfg;
cpu_sample_t t1, t2;
if (!g_cpu_have_prev) {
if (!read_cpu_sample(&t1)) return;
sleep(1);
if (!read_cpu_sample(&t2)) return;
} else {
t1 = g_cpu_prev;
if (!read_cpu_sample(&t2)) return;
}
g_cpu_prev = t2; g_cpu_have_prev = true;
long long idle_delta = (t2.idle + t2.iowait) - (t1.idle + t1.iowait);
long long total_delta = t2.total - t1.total;
if (total_delta == 0) return;
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "cpu_monitor");
kv_set_dbl(&d, "cpu_percent", round(100.0*(1.0-(double)idle_delta/total_delta)*10)/10.0);
kv_set_dbl(&d, "cpu_user", round(100.0*(t2.user - t1.user) / total_delta*10)/10.0);
kv_set_dbl(&d, "cpu_system", round(100.0*(t2.sys - t1.sys) / total_delta*10)/10.0);
kv_set_dbl(&d, "cpu_idle", round(100.0*(t2.idle - t1.idle) / total_delta*10)/10.0);
if (t2.iowait || t1.iowait)
kv_set_dbl(&d, "cpu_iowait", round(100.0*(t2.iowait - t1.iowait) / total_delta*10)/10.0);
double la[3];
if (getloadavg(la, 3) == 3) {
kv_set_dbl(&d, "load_1min", round(la[0]*100)/100.0);
kv_set_dbl(&d, "load_5min", round(la[1]*100)/100.0);
kv_set_dbl(&d, "load_15min", round(la[2]*100)/100.0);
}
read_cpu_extras(&d);
kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", cfg->cpu_interval);
conn_send(c, "PLG", &d);
LOGD("sent cpu_monitor");
}
/* ============================================================
* Plugin: memory_monitor
* Linux: /proc/meminfo
* FreeBSD: sysctl vm.stats.vm.*
* NetBSD: sysctl vm.uvmexp (struct uvmexp_sysctl)
* ============================================================ */
/* emit the common kvdict fields and send */
static void mem_send(conn_t *c,
long long tot, long long used, long long av, long long fr,
long long act, long long ina, long long cac, long long buf,
long long stot, long long sused, int interval) {
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "memory_monitor");
kv_set_ull(&d, "memory_total", (unsigned long long)tot);
kv_set_ull(&d, "memory_used", (unsigned long long)used);
kv_set_ull(&d, "memory_available", (unsigned long long)av);
kv_set_ull(&d, "memory_free", (unsigned long long)fr);
char pct[32];
snprintf(pct, sizeof(pct), "%.1f", tot ? 100.0*used/tot : 0.0);
kv_set(&d, "memory_percent", pct);
if (buf) kv_set_ull(&d, "memory_buffers", (unsigned long long)buf);
if (cac) kv_set_ull(&d, "memory_cached", (unsigned long long)cac);
if (act) kv_set_ull(&d, "memory_active", (unsigned long long)act);
if (ina) kv_set_ull(&d, "memory_inactive", (unsigned long long)ina);
if (stot > 0) {
long long sfr = stot - sused;
kv_set_ull(&d, "swap_total", (unsigned long long)stot);
kv_set_ull(&d, "swap_used", (unsigned long long)sused);
kv_set_ull(&d, "swap_free", (unsigned long long)sfr);
snprintf(pct, sizeof(pct), "%.1f", 100.0*sused/stot);
kv_set(&d, "swap_percent", pct);
}
kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", interval);
conn_send(c, "PLG", &d);
LOGD("sent memory_monitor");
}
#ifdef __linux__
static void plugin_memory_monitor(conn_t *c, const config_t *cfg) {
(void)cfg;
FILE *f = fopen("/proc/meminfo", "r");
if (!f) return;
long long tot=0, fr=0, av=0, buf=0, cac=0, act=0, ina=0, stot=0, sfr=0;
char line[256];
while (fgets(line, sizeof(line), f)) {
long long v; char key[64];
if (sscanf(line, "%63s %lld", key, &v) != 2) continue;
int kl = strlen(key); if (key[kl-1]==':') key[kl-1]='\0';
if (!strcmp(key,"MemTotal")) tot=v;
else if (!strcmp(key,"MemFree")) fr=v;
else if (!strcmp(key,"MemAvailable")) av=v;
else if (!strcmp(key,"Buffers")) buf=v;
else if (!strcmp(key,"Cached")) cac=v;
else if (!strcmp(key,"Active")) act=v;
else if (!strcmp(key,"Inactive")) ina=v;
else if (!strcmp(key,"SwapTotal")) stot=v;
else if (!strcmp(key,"SwapFree")) sfr=v;
}
fclose(f);
if (!tot) return;
if (!av) av = fr;
/* ZFS ARC is reclaimable memory not counted in MemAvailable */
long long arc_kb = 0;
FILE *arc = fopen("/proc/spl/kstat/zfs/arcstats", "r");
if (arc) {
char ak[64]; long long av2; int at;
while (fscanf(arc, "%63s %d %lld\n", ak, &at, &av2) == 3)
if (!strcmp(ak,"size")) { arc_kb = av2/1024; break; }
fclose(arc);
}
av = (av + arc_kb < tot) ? av + arc_kb : tot;
long long used = tot - av;
/* values from /proc/meminfo are in kB */
mem_send(c, tot*1024, used*1024, av*1024, fr*1024,
act*1024, ina*1024, cac*1024, buf*1024,
stot*1024, (stot-sfr)*1024, cfg->mem_interval);
}
#elif defined(__FreeBSD__) || defined(__DragonFly__)
static void plugin_memory_monitor(conn_t *c, const config_t *cfg) {
(void)cfg;
long ps = getpagesize();
unsigned long physmem = 0; size_t len = sizeof(physmem);
sysctlbyname("hw.physmem", &physmem, &len, NULL, 0);
if (!physmem) return;
unsigned int v_free=0, v_inactive=0, v_active=0, v_cache=0;
len = sizeof(v_free); sysctlbyname("vm.stats.vm.v_free_count", &v_free, &len, NULL, 0);
len = sizeof(v_inactive); sysctlbyname("vm.stats.vm.v_inactive_count", &v_inactive, &len, NULL, 0);
len = sizeof(v_active); sysctlbyname("vm.stats.vm.v_active_count", &v_active, &len, NULL, 0);
len = sizeof(v_cache); sysctlbyname("vm.stats.vm.v_cache_count", &v_cache, &len, NULL, 0);
long long tot = (long long)physmem;
long long fr = (long long)v_free * ps;
long long act = (long long)v_active * ps;
long long ina = (long long)v_inactive * ps;
long long cac = (long long)v_cache * ps;
long long av = fr + ina + cac; if (av > tot) av = tot;
long long used = tot - av;
mem_send(c, tot, used, av, fr, act, ina, cac, 0, 0, 0, cfg->mem_interval);
}
#elif defined(__NetBSD__)
static void plugin_memory_monitor(conn_t *c, const config_t *cfg) {
(void)cfg;
struct uvmexp_sysctl uvm;
size_t len = sizeof(uvm);
int mib[2] = {CTL_VM, VM_UVMEXP2};
if (sysctl(mib, 2, &uvm, &len, NULL, 0) != 0) return;
long long ps = uvm.pagesize;
long long tot = (long long)uvm.npages * ps;
long long fr = (long long)uvm.free * ps;
long long act = (long long)uvm.active * ps;
long long ina = (long long)uvm.inactive * ps;
long long av = fr + ina; if (av > tot) av = tot;
long long used = tot - av;
long long stot = (long long)uvm.swpages * ps;
long long sinuse = (long long)uvm.swpginuse * ps;
mem_send(c, tot, used, av, fr, act, ina, 0, 0, stot, sinuse, cfg->mem_interval);
}
#endif /* platform memory */
/* ============================================================
* Plugin: disk_monitor (df -P)
* ============================================================ */
static void plugin_disk_monitor(conn_t *c, const config_t *cfg) {
FILE *f = popen("df -P", "r");
if (!f) return;
char *json = malloc(MAX_VAL); if (!json) { pclose(f); return; }
int jlen = 0; bool first = true;
json[jlen++] = '{';
char line[512];
if (!fgets(line, sizeof(line), f)) { pclose(f); free(json); return; } /* skip header */
while (fgets(line, sizeof(line), f)) {
char fs[256], mount[256], pcts[16];
long long tk, uk, ak; int pct;
if (sscanf(line, "%255s %lld %lld %lld %15s %255s", fs, &tk, &uk, &ak, pcts, mount) != 6) continue;
pct = atoi(pcts);
if (cfg->disk_mount_count > 0) {
bool found = false;
for (int i = 0; i < cfg->disk_mount_count; i++)
if (!strcmp(cfg->disk_mounts[i], mount)) { found=true; break; }
if (!found) continue;
}
/* JSON-escape the mount point */
char esc[512]; int ei = 0;
for (int i = 0; mount[i] && ei < (int)sizeof(esc)-2; i++) {
if (mount[i]=='"'||mount[i]=='\\') esc[ei++]='\\';
esc[ei++] = mount[i];
}
esc[ei] = '\0';
char entry[512];
int el = snprintf(entry, sizeof(entry),
"%s\"%s\":{\"total\":%lld,\"used\":%lld,\"free\":%lld,\"percent\":%d}",
first ? "" : ",", esc, tk*1024, uk*1024, ak*1024, pct);
if (jlen + el < MAX_VAL - 2) { memcpy(json+jlen, entry, el); jlen+=el; first=false; }
}
pclose(f);
json[jlen++] = '}'; json[jlen] = '\0';
if (first) { free(json); return; }
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "disk_monitor");
char *jval = malloc(MAX_VAL + 1);
if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "partitions", jval); free(jval); }
kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", cfg->disk_interval);
conn_send(c, "PLG", &d);
free(json);
LOGD("sent disk_monitor");
}
/* ============================================================
* Plugin: network_monitor
* Linux: /proc/net/dev
* BSD: getifaddrs() + struct if_data (AF_LINK)
* ============================================================ */
typedef struct { char iface[64]; unsigned long long rx, tx; } net_stat_t;
static net_stat_t g_net_prev[64];
static int g_net_prev_n = 0;
static double g_net_prev_ts = 0;
#ifdef __linux__
static int read_net_stats(net_stat_t *out, int max) {
FILE *f = fopen("/proc/net/dev", "r");
if (!f) return 0;
char line[512]; int n = 0;
if (!fgets(line, sizeof(line), f)) { fclose(f); return 0; } /* header 1 */
if (!fgets(line, sizeof(line), f)) { fclose(f); return 0; } /* header 2 */
while (fgets(line, sizeof(line), f) && n < max) {
char *col = strchr(line, ':');
if (!col) continue;
*col = '\0';
char *iface = line; while (*iface==' ') iface++;
unsigned long long rx, tx, dummy;
if (sscanf(col+1, "%llu %llu %llu %llu %llu %llu %llu %llu %llu",
&rx,&dummy,&dummy,&dummy,&dummy,&dummy,&dummy,&dummy,&tx) == 9) {
snprintf(out[n].iface, 64, "%.*s", 63, iface);
out[n].rx = rx; out[n].tx = tx; n++;
}
}
fclose(f); return n;
}
#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
static int read_net_stats(net_stat_t *out, int max) {
struct ifaddrs *ifap;
if (getifaddrs(&ifap) != 0) return 0;
int n = 0;
for (struct ifaddrs *ifa = ifap; ifa && n < max; ifa = ifa->ifa_next) {
if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_LINK) continue;
struct if_data *ifd = (struct if_data *)ifa->ifa_data;
if (!ifd) continue;
snprintf(out[n].iface, 64, "%.*s", 63, ifa->ifa_name);
out[n].rx = (unsigned long long)ifd->ifi_ibytes;
out[n].tx = (unsigned long long)ifd->ifi_obytes;
n++;
}
freeifaddrs(ifap);
return n;
}
#endif /* platform network */
static void plugin_network_monitor(conn_t *c, const config_t *cfg) {
net_stat_t curr[64];
int cn = read_net_stats(curr, 64);
double now = now_ts();
if (g_net_prev_n == 0) {
memcpy(g_net_prev, curr, cn * sizeof(net_stat_t));
g_net_prev_n = cn; g_net_prev_ts = now;
return;
}
double dt = now - g_net_prev_ts;
if (dt <= 0) return;
char *json = malloc(MAX_VAL); if (!json) return;
int jlen = 0; bool first = true;
json[jlen++] = '{';
for (int i = 0; i < cn; i++) {
bool skip = false;
for (int s = 0; s < cfg->net_skip_count; s++)
if (!strcmp(cfg->net_skip[s], curr[i].iface)) { skip=true; break; }
if (skip) continue;
net_stat_t *prev = NULL;
for (int j = 0; j < g_net_prev_n; j++)
if (!strcmp(g_net_prev[j].iface, curr[i].iface)) { prev=&g_net_prev[j]; break; }
if (!prev) continue;
long long rd = (long long)(curr[i].rx - prev->rx);
long long td = (long long)(curr[i].tx - prev->tx);
char entry[256];
int el = snprintf(entry, sizeof(entry),
"%s\"%s\":{\"bytes_recv\":%llu,\"bytes_sent\":%llu"
",\"bytes_recv_delta\":%lld,\"bytes_sent_delta\":%lld}",
first?"":"," , curr[i].iface, curr[i].rx, curr[i].tx, rd, td);
if (jlen + el < MAX_VAL - 2) { memcpy(json+jlen, entry, el); jlen+=el; first=false; }
}
json[jlen++] = '}'; json[jlen] = '\0';
memcpy(g_net_prev, curr, cn * sizeof(net_stat_t));
g_net_prev_n = cn; g_net_prev_ts = now;
if (first) { free(json); return; }
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "network_monitor");
char *jval = malloc(MAX_VAL + 1);
if (jval) { snprintf(jval, MAX_VAL, "@%s", json); kv_set(&d, "interfaces", jval); free(jval); }
kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", cfg->net_interval);
conn_send(c, "PLG", &d);
free(json);
LOGD("sent network_monitor");
}
/* ============================================================
* Plugin: ping_monitor
* ============================================================ */
static bool do_ping(const char *host, int count, int timeout,
double *rmin, double *ravg, double *rmax, double *loss) {
*rmin = *ravg = *rmax = 1.0/0.0; /* inf */
*loss = 100.0;
char cmd[512];
snprintf(cmd, sizeof(cmd), "ping -c %d -W %d %s 2>/dev/null", count, timeout, host);
FILE *f = popen(cmd, "r");
if (!f) return false;
char line[512]; bool got = false;
while (fgets(line, sizeof(line), f)) {
char *p = strstr(line, "% packet loss");
if (p) {
while (p > line && (isdigit((unsigned char)*(p-1)) || *(p-1)=='.')) p--;
*loss = atof(p);
}
p = strstr(line, "min/avg/max");
if (p) {
p = strchr(p, '=');
if (p) { sscanf(p+1, " %lf/%lf/%lf", rmin, ravg, rmax); got=true; }
}
}
pclose(f); return got;
}
static void plugin_ping_monitor(conn_t *c, const config_t *cfg) {
if (!cfg->ping_host_count) return;
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "ping_monitor");
for (int i = 0; i < cfg->ping_host_count; i++) {
double rmin, ravg, rmax, loss;
bool ok = do_ping(cfg->ping_hosts[i], cfg->ping_count, cfg->ping_timeout,
&rmin, &ravg, &rmax, &loss);
char key[300];
snprintf(key, sizeof(key), "%s", cfg->ping_hosts[i]);
for (char *p = key; *p; p++) if (!isalnum((unsigned char)*p) && *p!='_') *p='_';
char kb[512];
if (ok) {
snprintf(kb,sizeof(kb),"%s_rtt_min",key); kv_set_dbl(&d,kb,rmin);
snprintf(kb,sizeof(kb),"%s_rtt_avg",key); kv_set_dbl(&d,kb,ravg);
snprintf(kb,sizeof(kb),"%s_rtt_max",key); kv_set_dbl(&d,kb,rmax);
snprintf(kb,sizeof(kb),"%s_loss",key); kv_set_dbl(&d,kb,loss);
} else {
snprintf(kb,sizeof(kb),"%s_rtt_min",key); kv_set(&d,kb,"inf");
snprintf(kb,sizeof(kb),"%s_rtt_avg",key); kv_set(&d,kb,"inf");
snprintf(kb,sizeof(kb),"%s_rtt_max",key); kv_set(&d,kb,"inf");
snprintf(kb,sizeof(kb),"%s_loss",key); kv_set_dbl(&d,kb,100.0);
}
}
kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", cfg->ping_interval);
conn_send(c, "PLG", &d);
LOGD("sent ping_monitor");
}
/* ============================================================
* Plugin: nagios_runner
* ============================================================ */
static const char *nagios_status_str(int rc) {
switch (rc) { case 0:return"OK"; case 1:return"WARNING"; case 2:return"CRITICAL"; default:return"UNKNOWN"; }
}
static void parse_perfdata(const char *output, kvdict_t *d, const char *prefix) {
const char *pipe = strchr(output, '|');
if (!pipe) return;
const char *p = pipe + 1;
while (*p) {
while (*p==' ') p++;
if (!*p) break;
char label[128]; int li = 0;
if (*p == '\'') {
p++;
while (*p && *p != '\'') label[li++] = *p++;
if (*p == '\'') p++;
} else {
while (*p && *p != '=') label[li++] = *p++;
}
label[li] = '\0';
if (*p != '=') { while (*p && *p!=' ') p++; continue; }
p++;
char val[64]; int vi = 0;
while (*p && (isdigit((unsigned char)*p) || *p=='.' || *p=='-')) val[vi++] = *p++;
val[vi] = '\0';
char uom[16]; int ui = 0;
while (*p && *p!=';' && *p!=' ') uom[ui++] = *p++;
uom[ui] = '\0';
char key[256];
snprintf(key, sizeof(key), "%s_%s", prefix, label); kv_set(d, key, val);
if (uom[0]) { snprintf(key,sizeof(key),"%s_%s_uom",prefix,label); kv_set(d,key,uom); }
for (int n=0; n<4 && *p==';'; n++) { p++; while (*p && *p!=';' && *p!=' ') p++; }
while (*p==' ') p++;
}
}
static void plugin_nagios_runner(conn_t *c, const config_t *cfg) {
if (!cfg->nagios_count) return;
kvdict_t d; kv_clear(&d);
kv_set(&d, "plugin", "nagios_runner");
for (int i = 0; i < cfg->nagios_count; i++) {
const char *name = cfg->nagios[i].name;
char output[4096] = ""; int rc = 3;
FILE *f = popen(cfg->nagios[i].cmd, "r");
if (f) {
size_t _nr = fread(output, 1, sizeof(output)-1, f); (void)_nr;
int ret = pclose(f);
rc = WIFEXITED(ret) ? WEXITSTATUS(ret) : 3;
if (rc < 0 || rc > 3) rc = 3;
}
char *nl = strchr(output, '\n'); if (nl) *nl = '\0';
char msg[512]; char *pipe = strchr(output,'|');
if (pipe) snprintf(msg, sizeof(msg), "%.*s", (int)(pipe-output), output);
else snprintf(msg, sizeof(msg), "%s", output);
char key[128];
snprintf(key,sizeof(key),"%s_status",name); kv_set(&d,key,nagios_status_str(rc));
snprintf(key,sizeof(key),"%s_status_code",name); kv_set_int(&d,key,rc);
snprintf(key,sizeof(key),"%s_output",name); kv_set(&d,key,msg);
parse_perfdata(output, &d, name);
}
kv_set_dbl(&d, "_timestamp", now_ts());
kv_set_int(&d, "_interval", cfg->nagios_interval);
conn_send(c, "PLG", &d);
LOGD("sent nagios_runner");
}
/* ============================================================
* Monitor thread: all periodic plugins in one thread
* ============================================================ */
typedef struct { conn_t *conn; const config_t *cfg; } thread_ctx_t;
static void *monitor_thread(void *arg) {
thread_ctx_t *ctx = arg;
conn_t *conn = ctx->conn;
const config_t *cfg = ctx->cfg;
time_t next_cpu = time(NULL);
time_t next_mem = time(NULL);
time_t next_disk = time(NULL);
time_t next_net = time(NULL);
time_t next_ping = time(NULL);
time_t next_nagios = time(NULL);
/* Prime network baseline */
plugin_network_monitor(conn, cfg);
while (g_running) {
time_t now = time(NULL);
if (now >= next_cpu) { plugin_cpu_monitor(conn, cfg); next_cpu = now + cfg->cpu_interval; }
if (now >= next_mem) { plugin_memory_monitor(conn, cfg); next_mem = now + cfg->mem_interval; }
if (now >= next_disk) { plugin_disk_monitor(conn, cfg); next_disk = now + cfg->disk_interval; }
if (now >= next_net) { plugin_network_monitor(conn, cfg); next_net = now + cfg->net_interval; }
if (cfg->ping_host_count && now >= next_ping) {
plugin_ping_monitor(conn, cfg); next_ping = now + cfg->ping_interval;
}
if (cfg->nagios_count && now >= next_nagios) {
plugin_nagios_runner(conn, cfg); next_nagios = now + cfg->nagios_interval;
}
sleep(1);
}
return NULL;
}
/* ============================================================
* Daemonize
* ============================================================ */
static void daemonize(void) {
for (int n = 0; n < 2; n++) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid > 0) exit(0);
if (n == 0) { setsid(); if (chdir("/")) {}; umask(0); }
}
int fd;
fd = open("/dev/zero", O_RDONLY); if (fd>=0) { dup2(fd,0); close(fd); }
fd = open("/dev/null", O_WRONLY); if (fd>=0) { dup2(fd,1); close(fd); }
fd = open("/dev/null", O_WRONLY); if (fd>=0) { dup2(fd,2); close(fd); }
}
/* ============================================================
* CLI
* ============================================================ */
static void usage(const char *prog) {
fprintf(stderr,
"Usage: %s [options] host [host...]\n"
" -b Send boot message\n"
" -c FILE Config file (JSON)\n"
" -m MSG Send one-shot message\n"
" -n NAME Override hostname\n"
" -4 Use IPv4 only\n"
" -6 Use IPv6 only\n"
" -d Daemonize\n"
" -v Verbose (info)\n"
" -x Debug\n"
" -h Help\n", prog);
}
int main(int argc, char **argv) {
bool do_boot = false;
bool do_daemon = false;
const char *cfgpath = NULL;
const char *message = NULL;
const char *nameov = NULL;
int af_filter = 0;
int opt;
while ((opt = getopt(argc, argv, "bc:m:n:dvxh46")) != -1) {
switch (opt) {
case 'b': do_boot = true; break;
case 'c': cfgpath = optarg; break;
case 'm': message = optarg; break;
case 'n': nameov = optarg; break;
case 'd': do_daemon = true; break;
case 'v': g_log_level = LL_INFO; break;
case 'x': g_log_level = LL_DEBUG; break;
case '4': af_filter = AF_INET; break;
case '6': af_filter = AF_INET6; break;
case 'h': usage(argv[0]); return 0;
default: usage(argv[0]); return 1;
}
}
if (optind >= argc) { fprintf(stderr,"error: host required\n"); usage(argv[0]); return 1; }
const char *hosts[MAX_HOSTS]; int nhost = 0;
for (int i = optind; i < argc && nhost < MAX_HOSTS; i++) hosts[nhost++] = argv[i];
config_t cfg;
config_load(&cfg, cfgpath);
if (do_daemon) {
daemonize();
g_use_syslog = true;
openlog("hbc", LOG_PID, LOG_DAEMON);
}
char iam[256];
if (nameov) {
snprintf(iam, sizeof(iam), "%s", nameov);
} else {
gethostname(iam, sizeof(iam));
char *dot = strchr(iam, '.'); if (dot) *dot = '\0';
}
struct sigaction sa = {0};
sa.sa_handler = sig_handler;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGHUP, &sa, NULL);
int conn_id = 1;
int retry_delay = 5;
while (g_running && !g_nconns) {
for (int i = 0; i < nhost; i++) {
struct addrinfo hints = {0}, *res = NULL;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
hints.ai_family = af_filter;
char ps[16]; snprintf(ps, sizeof(ps), "%d", cfg.hb_port);
if (getaddrinfo(hosts[i], ps, &hints, &res) != 0) {
LOGW("cannot resolve %s — retrying in %ds", hosts[i], retry_delay);
continue;
}
for (struct addrinfo *ai = res; ai && g_nconns < MAX_HOSTS; ai = ai->ai_next) {
conn_t *c = &g_conns[g_nconns];
memset(c, 0, sizeof(*c));
c->conn_id = conn_id++; c->port = cfg.hb_port;
c->af = ai->ai_family; c->sockfd = -1;
snprintf(c->name, sizeof(c->name), "%s", iam);
void *addr = (ai->ai_family == AF_INET)
? (void *)&((struct sockaddr_in *)ai->ai_addr)->sin_addr
: (void *)&((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr;
inet_ntop(ai->ai_family, addr, c->addr, sizeof(c->addr));
if (conn_open(c)) { g_nconns++; LOGI("connected to %s", c->addr); }
}
freeaddrinfo(res);
}
if (!g_nconns) {
sleep(retry_delay);
if (retry_delay < 60) retry_delay *= 2;
}
}
if (!g_nconns) return 1;
conn_t *primary = &g_conns[0];
LOGI("hbc_mini-c %s on %s -> %s port=%d interval=%ds",
HBC_VERSION, iam, hosts[0], cfg.hb_port, cfg.interval);
/* Boot / one-shot message */
bool send_shutdown = false;
if (do_boot || message) {
kvdict_t bm; kv_clear(&bm);
kv_set_int(&bm, "acks", 0);
if (do_boot) { kv_set_int(&bm,"boot",1); send_shutdown=true; }
if (message) { kv_set(&bm,"service","service"); kv_set(&bm,"msg",message); }
conn_send(primary, "HTB", &bm);
if (message && !do_daemon) {
usleep(300000);
for (int i=0;i<g_nconns;i++) conn_close(&g_conns[i]);
return 0;
}
}
/* Info plugins at startup */
plugin_os_info(primary, &cfg);
/* Monitor thread */
thread_ctx_t tctx = {primary, &cfg};
pthread_t tid;
pthread_create(&tid, NULL, monitor_thread, &tctx);
/* Main heartbeat + receive loop */
time_t last_hb = 0;
while (g_running) {
time_t now = time(NULL);
if (now - last_hb >= cfg.interval) {
for (int i = 0; i < g_nconns; i++) {
kvdict_t hb; kv_clear(&hb);
kv_set_int(&hb, "acks", g_conns[i].ack_count);
kv_set_dbl(&hb, "rtt", g_conns[i].rtt);
kv_set_int(&hb, "interval", cfg.interval);
conn_send(&g_conns[i], "HTB", &hb);
}
last_hb = now;
}
fd_set rset; FD_ZERO(&rset); int maxfd = -1;
for (int i = 0; i < g_nconns; i++) {
if (g_conns[i].sockfd >= 0) { FD_SET(g_conns[i].sockfd,&rset); if(g_conns[i].sockfd>maxfd) maxfd=g_conns[i].sockfd; }
}
if (maxfd >= 0) {
struct timeval tv = {1, 0};
if (select(maxfd+1, &rset, NULL, NULL, &tv) > 0)
for (int i = 0; i < g_nconns; i++)
if (g_conns[i].sockfd>=0 && FD_ISSET(g_conns[i].sockfd,&rset))
conn_recv(&g_conns[i]);
} else { sleep(1); }
if (primary->request_info) {
primary->request_info = false;
LOGI("refreshing info plugins on server request");
plugin_os_info(primary, &cfg);
}
}
pthread_cancel(tid);
pthread_join(tid, NULL);
if (send_shutdown) {
kvdict_t sh; kv_clear(&sh);
kv_set_int(&sh, "shutdown", 1);
kv_set_int(&sh, "acks", primary->ack_count);
conn_send(primary, "HTB", &sh);
usleep(300000);
}
for (int i = 0; i < g_nconns; i++) conn_close(&g_conns[i]);
if (g_use_syslog) closelog();
if (g_restart) { LOGI("restarting..."); execv(argv[0], argv); }
return 0;
}