From 44e75573accbe0e529f3754f78d1579673c4e6de Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Fri, 27 Feb 2026 16:52:16 -0600 Subject: [PATCH] examples/lib/live: a lib example with live capture Simple libpcap example for live capture. Allows listening on multiple interfaces to show how multiple threads (workers) can be used. Ticket: #8096 (cherry picked from commit f711e57e8e0f3ea2d49b97c7fd5afc28e32711d2) --- .github/workflows/builds.yml | 4 + Makefile.am | 3 +- configure.ac | 1 + examples/lib/live/.gitignore | 3 + examples/lib/live/Makefile.am | 9 + examples/lib/live/Makefile.example.in | 17 ++ examples/lib/live/README.md | 75 ++++++ examples/lib/live/main.c | 355 ++++++++++++++++++++++++++ 8 files changed, 466 insertions(+), 1 deletion(-) create mode 100644 examples/lib/live/.gitignore create mode 100644 examples/lib/live/Makefile.am create mode 100644 examples/lib/live/Makefile.example.in create mode 100644 examples/lib/live/README.md create mode 100644 examples/lib/live/main.c diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 1a8be2f230..e3d29fa501 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -226,6 +226,10 @@ jobs: test $(cat eve.json |jq 'select(.stats) | .stats.decoder.pkts') = 110 working-directory: examples/lib/custom + - name: Build live library example + run: make + working-directory: examples/lib/live + - name: Cleaning source directory for standalone plugin test. run: make clean diff --git a/Makefile.am b/Makefile.am index 7e8c37e9fa..e99cea855b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -14,7 +14,8 @@ EXTRA_DIST = ChangeLog COPYING LICENSE suricata.yaml.in \ examples/plugins SUBDIRS = rust src plugins qa rules doc etc python ebpf \ $(SURICATA_UPDATE_DIR) -DIST_SUBDIRS = $(SUBDIRS) examples/lib/simple examples/lib/custom +DIST_SUBDIRS = $(SUBDIRS) examples/lib/simple examples/lib/custom \ + examples/lib/live CLEANFILES = stamp-h[0-9]* diff --git a/configure.ac b/configure.ac index 6cca8b9ed4..667b5a49ac 100644 --- a/configure.ac +++ b/configure.ac @@ -2583,6 +2583,7 @@ AC_CONFIG_FILES(examples/plugins/ci-capture/Makefile) AC_CONFIG_FILES(examples/lib/simple/Makefile examples/lib/simple/Makefile.example) AC_CONFIG_FILES(examples/lib/custom/Makefile examples/lib/custom/Makefile.example) AC_CONFIG_FILES(examples/lib/cplusplus/Makefile.example) +AC_CONFIG_FILES(examples/lib/live/Makefile examples/lib/live/Makefile.example) AC_CONFIG_FILES(plugins/Makefile) AC_CONFIG_FILES(plugins/pfring/Makefile) AC_CONFIG_FILES(plugins/napatech/Makefile) diff --git a/examples/lib/live/.gitignore b/examples/lib/live/.gitignore new file mode 100644 index 0000000000..82b104723c --- /dev/null +++ b/examples/lib/live/.gitignore @@ -0,0 +1,3 @@ +!/Makefile.example.in +Makefile.example +/live diff --git a/examples/lib/live/Makefile.am b/examples/lib/live/Makefile.am new file mode 100644 index 0000000000..904d78e99a --- /dev/null +++ b/examples/lib/live/Makefile.am @@ -0,0 +1,9 @@ +bin_PROGRAMS = live + +live_SOURCES = main.c + +AM_CPPFLAGS = -I$(top_srcdir)/src + +live_LDFLAGS = $(all_libraries) $(SECLDFLAGS) +live_LDADD = "-Wl,--start-group,$(top_builddir)/src/libsuricata_c.a,../../$(RUST_SURICATA_LIB),--end-group" $(RUST_LDADD) +live_DEPENDENCIES = $(top_builddir)/src/libsuricata_c.a ../../$(RUST_SURICATA_LIB) diff --git a/examples/lib/live/Makefile.example.in b/examples/lib/live/Makefile.example.in new file mode 100644 index 0000000000..cd07a1a758 --- /dev/null +++ b/examples/lib/live/Makefile.example.in @@ -0,0 +1,17 @@ +LIBSURICATA_CONFIG ?= @CONFIGURE_PREFIX@/bin/libsuricata-config + +# Define STATIC=1 to request static linking where available +SURICATA_LIBS = `$(LIBSURICATA_CONFIG) --libs $${STATIC:+--static}` +SURICATA_CFLAGS := `$(LIBSURICATA_CONFIG) --cflags` + +# Currently the Suricata logging system requires this to be even for +# plugins. +CPPFLAGS += "-D__SCFILENAME__=\"$(*F)\"" + +all: live + +live: main.c + $(CC) -o $@ $^ $(CPPFLAGS) $(CFLAGS) $(SURICATA_CFLAGS) $(SURICATA_LIBS) + +clean: + rm -f live diff --git a/examples/lib/live/README.md b/examples/lib/live/README.md new file mode 100644 index 0000000000..0e5683576d --- /dev/null +++ b/examples/lib/live/README.md @@ -0,0 +1,75 @@ +# Live Capture Library Example + +This is an example of using the Suricata library to capture live +traffic from a network interface with custom packet handling and +threading. + +## Building In Tree + +The Suricata build system has created a Makefile that should allow you +to build this application in-tree on most supported platforms. To +build simply run: + +``` +make +``` + +## Running + +``` +./live -i eth0 -l . +``` + +This example requires at least one `-i` option to specify the network +interface to capture from. You can specify multiple interfaces to +capture from multiple sources simultaneously - a separate worker thread +will be created for each interface: + +``` +./live -i eth0 -i eth1 +``` + +Any additional arguments are passed directly to Suricata as command +line arguments. + +Example with common options: +``` +sudo ./live -i eth0 -- -l . -S rules.rules +``` + +Example capturing from multiple interfaces: +``` +sudo ./live -i eth0 -i wlan0 -- -l . -S rules.rules +``` + +Shutdown: each worker thread may call EngineStop when its capture ends; the +main loop waits for this signal, performs SuricataShutdown concurrently with +per-thread SCTmThreadsSlotPacketLoopFinish, then joins all worker threads +before GlobalsDestroy. + +The example supports up to 16 interfaces simultaneously. + +## Building Out of Tree + +A Makefile.example has also been generated to use as an example on how +to build against the library in a standalone application. + +First build and install the Suricata library including: + +``` +make install-library +make install-headers +``` + +Then run: + +``` +make -f Makefile.example +``` + +If you installed to a non-standard location, you need to ensure that +`libsuricata-config` is in your path, for example: + +``` +PATH=/opt/suricata/bin:$PATH make -f Makefile.example +``` diff --git a/examples/lib/live/main.c b/examples/lib/live/main.c new file mode 100644 index 0000000000..ff509f40ef --- /dev/null +++ b/examples/lib/live/main.c @@ -0,0 +1,355 @@ +/* Copyright (C) 2025 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +#include "suricata.h" +#include "detect.h" +#include "detect-engine.h" +#include "runmodes.h" +#include "conf.h" +#include "pcap.h" +#include "runmode-lib.h" +#include "tm-threads.h" +#include "threadvars.h" +#include "action-globals.h" +#include "packet.h" +#include "util-device.h" + +#include +#include + +/* Support up to 16 interfaces */ +#define MAX_INTERFACES 16 + +static int worker_id = 1; + +/* Interfaces parsed from command line, used by the runmode setup + * callback. */ +static char *g_interfaces[MAX_INTERFACES]; +static int g_interface_count = 0; + +/* ThreadVars created during runmode setup, before thread sealing. */ +static ThreadVars *g_worker_tvs[MAX_INTERFACES]; + +/** + * Struct to pass arguments into a worker thread. + */ +struct WorkerArgs { + ThreadVars *tv; + char *interface; + char *device_name; +}; + +/** + * Release packet callback. + * + * If there is any cleanup that needs to be done when Suricata is done + * with a packet, this is the place to do it. + * + * Important: If using a custom release function, you must also + * release or free the packet. + * + * Optionally this is where you would handle IPS like functionality + * such as forwarding the packet, or triggering some other mechanism + * to forward the packet. + */ +static void ReleasePacket(Packet *p) +{ + if (PacketCheckAction(p, ACTION_DROP)) { + SCLogNotice("Dropping packet!"); + } + + /* As we overode the default release function, we must release or + * free the packet. */ + PacketFreeOrRelease(p); +} + +/** + * Suricata worker thread in library mode. + * The functions should be wrapped in an API layer. + */ +static void *SimpleWorker(void *arg) +{ + struct WorkerArgs *args = arg; + ThreadVars *tv = args->tv; + int exit_code = EXIT_SUCCESS; + + /* Start worker. */ + if (SCRunModeLibSpawnWorker(tv) != 0) { + pthread_exit((void *)(intptr_t)EXIT_FAILURE); + } + + /* Open live capture on interface. */ + char errbuf[PCAP_ERRBUF_SIZE]; + pcap_t *fp = pcap_open_live(args->interface, 65535, 1, 1000, errbuf); + if (fp == NULL) { + SCLogError("Failed to open interface: %s", errbuf); + exit_code = EXIT_FAILURE; + goto done; + } + + LiveDevice *device = LiveGetDevice(args->device_name); + assert(device != NULL); + + int datalink = pcap_datalink(fp); + struct pcap_pkthdr *pkthdr; + const u_char *packet; + int pcap_rc; + + while (1) { + /* Have we been asked to stop? */ + if (suricata_ctl_flags & SURICATA_STOP) { + goto done; + } + + pcap_rc = pcap_next_ex(fp, &pkthdr, &packet); + if (pcap_rc == 0) { + /* Timeout - no packet available, continue waiting */ + continue; + } else if (pcap_rc == -1) { + /* Error occurred */ + SCLogError("pcap_next_ex failed on %s: %s", args->interface, pcap_geterr(fp)); + exit_code = EXIT_FAILURE; + goto done; + } else if (pcap_rc == -2) { + /* End of file (shouldn't happen in live capture) */ + SCLogNotice("End of capture on %s", args->interface); + exit_code = EXIT_FAILURE; + goto done; + } + + Packet *p = PacketGetFromQueueOrAlloc(); + if (unlikely(p == NULL)) { + /* Memory allocation error: backoff and continue instead of stopping */ + usleep(1000); /* brief sleep to avoid tight spin */ + continue; + } + + /* Setup the packet, these will become functions to avoid + * internal Packet access. */ + SCPacketSetSource(p, PKT_SRC_WIRE); + SCPacketSetTime(p, SCTIME_FROM_TIMEVAL(&pkthdr->ts)); + SCPacketSetDatalink(p, datalink); + SCPacketSetLiveDevice(p, device); + SCPacketSetReleasePacket(p, ReleasePacket); + + if (PacketSetData(p, packet, pkthdr->len) == -1) { + TmqhOutputPacketpool(tv, p); + /* Bad packet or setup error; log and continue */ + SCLogDebug("PacketSetData failed on %s", args->interface); + continue; + } + + if (TmThreadsSlotProcessPkt(tv, tv->tm_slots, p) != TM_ECODE_OK) { + TmqhOutputPacketpool(tv, p); + /* Processing failure for this packet; continue capture */ + SCLogDebug("TmThreadsSlotProcessPkt failed on %s", args->interface); + continue; + } + + LiveDevicePktsIncr(device); + } + +done: + if (fp != NULL) { + pcap_close(fp); + } + + /* Signal main loop to shutdown. */ + EngineStop(); + + /* Cleanup. + * + * Note that there is some thread synchronization between this + * function and SuricataShutdown such that they must be run + * concurrently at this time before either will exit. */ + SCTmThreadsSlotPacketLoopFinish(tv); + + SCLogNotice("Worker thread exiting"); + pthread_exit((void *)(intptr_t)exit_code); +} + +static uint8_t RateFilterCallback(const Packet *p, const uint32_t sid, const uint32_t gid, + const uint32_t rev, uint8_t original_action, uint8_t new_action, void *arg) +{ + /* Don't change the action. */ + return new_action; +} + +/** + * Application runmode setup that creates ThreadVars before thread + * sealing. This follows the pattern used by other runmodes where + * threads are registered during the runmode callback. + */ +static int AppRunModeSetup(void) +{ + for (int i = 0; i < g_interface_count; i++) { + g_worker_tvs[i] = SCRunModeLibCreateThreadVars(worker_id++); + if (!g_worker_tvs[i]) { + SCLogError("Failed to create ThreadVars for interface %s", g_interfaces[i]); + return -1; + } + } + + return 0; +} + +int main(int argc, char **argv) +{ + int opt; + + /* Parse command line options using getopt */ + while ((opt = getopt(argc, argv, "i:")) != -1) { + switch (opt) { + case 'i': + if (g_interface_count >= MAX_INTERFACES) { + fprintf(stderr, "ERROR: Maximum %d interfaces supported\n", MAX_INTERFACES); + exit(EXIT_FAILURE); + } + g_interfaces[g_interface_count++] = optarg; + break; + default: + fprintf(stderr, "Usage: %s -i interface [-i interface2 ...] [suricata_options]\n", + argv[0]); + fprintf(stderr, " -i interface Network interface to capture from (can be " + "specified multiple times)\n"); + exit(EXIT_FAILURE); + } + } + + if (g_interface_count == 0) { + fprintf(stderr, "ERROR: At least one interface (-i) is required\n"); + fprintf(stderr, "Usage: %s -i interface [-i interface2 ...] [suricata_options]\n", argv[0]); + exit(EXIT_FAILURE); + } + + SuricataPreInit(argv[0]); + + /* Pass through the arguments after -- to Suricata. */ + char *suricata_argv[argc - optind + 2]; + int suricata_argc = 0; + suricata_argv[suricata_argc++] = argv[0]; + while (optind < argc) { + suricata_argv[suricata_argc++] = argv[optind++]; + } + suricata_argv[suricata_argc] = NULL; + optind = 1; + + /* Log the command line arguments being passed to Suricata */ + if (suricata_argc > 1) { + fprintf(stderr, "Passing command line arguments to Suricata:"); + for (int i = 1; i < suricata_argc; i++) { + fprintf(stderr, " %s", suricata_argv[i]); + } + fprintf(stderr, "\n"); + } + + SCParseCommandLine(suricata_argc, suricata_argv); + + /* Set the runmode to library mode. Perhaps in the future this + * should be done in some library bootstrap function. */ + SCRunmodeSet(RUNMODE_LIB); + + /* Validate/finalize the runmode. */ + if (SCFinalizeRunMode() != TM_ECODE_OK) { + exit(EXIT_FAILURE); + } + + /* Load configuration file, could be done earlier but must be done + * before SuricataInit, but even then its still optional as you + * may be programmatically configuration Suricata. */ + if (SCLoadYamlConfig() != TM_ECODE_OK) { + exit(EXIT_FAILURE); + } + + /* Enable default signal handlers including SIGHUP for log file rotation, + * and SIGUSR2 for reloading rules. This should be done with care by a + * library user as the application may already have signal handlers + * loaded. */ + SCEnableDefaultSignalHandlers(); + + /* Register our own library run mode. At this time, the ThreadVars + * for each capture thread need to be created in the provided + * callback to meet thread synchronization requirements. */ + RunModeRegisterNewRunMode( + RUNMODE_LIB, "live", "Live capture application run mode", AppRunModeSetup, NULL); + + if (!SCConfSetFromString("runmode=live", 1)) { + exit(EXIT_FAILURE); + } + + /* Force logging to the current directory. */ + SCConfSetFromString("default-log-dir=.", 1); + + /* Register a LiveDevice for each interface */ + for (int i = 0; i < g_interface_count; i++) { + if (LiveRegisterDevice(g_interfaces[i]) < 0) { + FatalError("LiveRegisterDevice failed for %s", g_interfaces[i]); + } + SCLogNotice("Registered device %s", g_interfaces[i]); + } + + /* SuricataInit will call our AppRunModeSetup, when it returns our + * ThreadVars will be ready. */ + SuricataInit(); + + SCDetectEngineRegisterRateFilterCallback(RateFilterCallback, NULL); + + /* Spawn our worker threads, one for each interface. */ + pthread_t workers[MAX_INTERFACES]; + struct WorkerArgs worker_args[MAX_INTERFACES]; + + for (int i = 0; i < g_interface_count; i++) { + worker_args[i].tv = g_worker_tvs[i]; + worker_args[i].interface = g_interfaces[i]; + worker_args[i].device_name = g_interfaces[i]; + + if (pthread_create(&workers[i], NULL, SimpleWorker, &worker_args[i]) != 0) { + FatalError("Failed to create worker thread for interface %s", g_interfaces[i]); + } + + SCLogNotice("Started worker thread for interface %s", g_interfaces[i]); + } + + SuricataPostInit(); + + /* Run the main loop, this just waits for the worker threads to + * call EngineStop signalling Suricata that it is done capturing + * from the interfaces. */ + SuricataMainLoop(); + + /* Shutdown engine. */ + SCLogNotice("Shutting down"); + + /* Note that there is some thread synchronization between this + * function and SCTmThreadsSlotPacketLoopFinish that require them + * to be run concurrently at this time. */ + SuricataShutdown(); + + /* Ensure our capture workers have fully exited before teardown. */ + int exit_status = EXIT_SUCCESS; + for (int i = 0; i < g_interface_count; i++) { + void *worker_status; + pthread_join(workers[i], &worker_status); + if ((intptr_t)worker_status != EXIT_SUCCESS) { + exit_status = EXIT_FAILURE; + } + } + + GlobalsDestroy(); + + return exit_status; +}