You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
suricata/src/source-pcap-file-helper.c

1047 lines
32 KiB
C

/* Copyright (C) 2007-2026 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.
*/
/**
* \file
*
* \author Danny Browning <danny.browning@protectwise.com>
*
* File based pcap packet acquisition support
*/
#include "source-pcap-file-helper.h"
#include "suricata.h"
#include "util-datalink.h"
#include "util-checksum.h"
#include "util-profiling.h"
#include "util-exception-policy.h"
#include "conf-yaml-loader.h"
#include "capture-hooks.h"
#include "threads.h"
extern uint32_t max_pending_packets;
extern PcapFileGlobalVars pcap_g;
static PcapFileFileVars *pcap_current_pfv = NULL;
static void PcapFileCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt);
static void PcapFileReleasePacket(Packet *p)
{
PcapFileFileVars *pfv = p->pcap_v.pfv;
if (pfv != NULL) {
PcapFileFinalizePacket(pfv);
}
PacketFreeOrRelease(p);
}
void PcapFileReleasePseudoPacket(Packet *p)
{
PcapFileFileVars *pfv = p->pcap_v.pfv;
/* Alerts are counted in PacketAlertFinalize via PcapFileAddAlertCount, so
* avoid double-counting here. Decrement refcount if we held one. */
if (pfv != NULL) {
uint32_t prev = SC_ATOMIC_SUB(pfv->ref_cnt, 1);
if (prev == 1 && pfv->cleanup_requested) {
CleanupPcapFileFileVars(pfv);
}
}
PacketFreeOrRelease(p);
}
void CleanupPcapFileFileVars(PcapFileFileVars *pfv)
{
if (pfv == NULL) {
return;
}
/* If there are still packets in flight, defer ALL cleanup actions, including
* the deletion decision, until the last packet completes. */
if (SC_ATOMIC_GET(pfv->ref_cnt) != 0) {
pfv->cleanup_requested = true;
return;
}
/* No packets in flight anymore: it's now safe to close, decide, and delete. */
if (pfv->pcap_handle != NULL) {
pcap_close(pfv->pcap_handle);
pfv->pcap_handle = NULL;
}
if (pfv->filename != NULL) {
if (PcapFileShouldDeletePcapFile(pfv)) {
SCLogDebug("Deleting pcap file %s", pfv->filename);
if (unlink(pfv->filename) != 0) {
SCLogWarning("Failed to delete %s: %s", pfv->filename, strerror(errno));
}
}
SCFree(pfv->filename);
pfv->filename = NULL;
}
pfv->shared = NULL;
if (pcap_current_pfv == pfv) {
pcap_current_pfv = NULL;
}
SCFree(pfv);
}
void PcapFileCallbackLoop(char *user, struct pcap_pkthdr *h, u_char *pkt)
{
SCEnter();
#ifdef QA_SIMULATION
if (unlikely((pcap_g.cnt + 1ULL) == g_eps_pcap_packet_loss)) {
SCLogNotice("skipping packet %" PRIu64, g_eps_pcap_packet_loss);
pcap_g.cnt++;
SCReturn;
}
#endif
PcapFileFileVars *ptv = (PcapFileFileVars *)user;
Packet *p = PacketGetFromQueueOrAlloc();
if (unlikely(p == NULL)) {
SCReturn;
}
SC_ATOMIC_ADD(ptv->ref_cnt, 1);
SCLogDebug("pcap-file: got packet, pfv=%p filename=%s ref_cnt now=%u p=%p", (void *)ptv,
ptv->filename, SC_ATOMIC_GET(ptv->ref_cnt), (void *)p);
PACKET_PROFILING_TMM_START(p, TMM_RECEIVEPCAPFILE);
PKT_SET_SRC(p, PKT_SRC_WIRE);
p->ts = SCTIME_FROM_TIMEVAL_UNTRUSTED(&h->ts);
SCLogDebug("p->ts.tv_sec %" PRIuMAX "", (uintmax_t)SCTIME_SECS(p->ts));
p->datalink = ptv->datalink;
p->pcap_v.pcap_cnt = ++pcap_g.cnt;
p->pcap_v.tenant_id = ptv->shared->tenant_id;
p->pcap_v.pfv = ptv;
ptv->shared->pkts++;
ptv->shared->bytes += h->caplen;
p->ReleasePacket = PcapFileReleasePacket;
if (unlikely(PacketCopyData(p, pkt, h->caplen))) {
TmqhOutputPacketpool(ptv->shared->tv, p);
PACKET_PROFILING_TMM_END(p, TMM_RECEIVEPCAPFILE);
SCReturn;
}
/* We only check for checksum disable */
if (pcap_g.checksum_mode == CHECKSUM_VALIDATION_DISABLE) {
p->flags |= PKT_IGNORE_CHECKSUM;
} else if (pcap_g.checksum_mode == CHECKSUM_VALIDATION_AUTO) {
if (ChecksumAutoModeCheck(ptv->shared->pkts, p->pcap_v.pcap_cnt,
SC_ATOMIC_GET(pcap_g.invalid_checksums))) {
pcap_g.checksum_mode = CHECKSUM_VALIDATION_DISABLE;
p->flags |= PKT_IGNORE_CHECKSUM;
}
}
PACKET_PROFILING_TMM_END(p, TMM_RECEIVEPCAPFILE);
if (TmThreadsSlotProcessPkt(ptv->shared->tv, ptv->shared->slot, p) != TM_ECODE_OK) {
pcap_breakloop(ptv->pcap_handle);
ptv->shared->cb_result = TM_ECODE_FAILED;
}
SCReturn;
}
char pcap_filename[PATH_MAX] = "unknown";
const char *PcapFileGetFilename(void)
{
return pcap_filename;
}
void PcapFileSetCurrentPfv(PcapFileFileVars *pfv)
{
pcap_current_pfv = pfv;
}
PcapFileFileVars *PcapFileGetCurrentPfv(void)
{
return pcap_current_pfv;
}
/**
* \brief Main PCAP file reading Loop function
*/
TmEcode PcapFileDispatch(PcapFileFileVars *ptv)
{
SCEnter();
PcapFileSetCurrentPfv(ptv);
/* initialize all the thread's initial timestamp */
if (likely(ptv->first_pkt_hdr != NULL)) {
TmThreadsInitThreadsTimestamp(SCTIME_FROM_TIMEVAL(&ptv->first_pkt_ts));
PcapFileCallbackLoop((char *)ptv, ptv->first_pkt_hdr,
(u_char *)ptv->first_pkt_data);
ptv->first_pkt_hdr = NULL;
ptv->first_pkt_data = NULL;
}
int packet_q_len = 64;
TmEcode loop_result = TM_ECODE_OK;
strlcpy(pcap_filename, ptv->filename, sizeof(pcap_filename));
while (loop_result == TM_ECODE_OK) {
if (suricata_ctl_flags & SURICATA_STOP) {
SCReturnInt(TM_ECODE_OK);
}
/* make sure we have at least one packet in the packet pool, to prevent
* us from alloc'ing packets at line rate */
PacketPoolWait();
/* Right now we just support reading packets one at a time. */
int r = pcap_dispatch(ptv->pcap_handle, packet_q_len,
(pcap_handler)PcapFileCallbackLoop, (u_char *)ptv);
if (unlikely(r == -1)) {
SCLogError("error code %" PRId32 " %s for %s", r, pcap_geterr(ptv->pcap_handle),
ptv->filename);
if (ptv->shared->cb_result == TM_ECODE_FAILED) {
SCReturnInt(TM_ECODE_FAILED);
}
loop_result = TM_ECODE_DONE;
} else if (unlikely(r == 0)) {
SCLogInfo("pcap file %s end of file reached (pcap err code %" PRId32 ")",
ptv->filename, r);
ptv->shared->files++;
loop_result = TM_ECODE_DONE;
} else if (ptv->shared->cb_result == TM_ECODE_FAILED) {
SCLogError("Pcap callback PcapFileCallbackLoop failed for %s", ptv->filename);
loop_result = TM_ECODE_FAILED;
}
StatsSyncCountersIfSignalled(&ptv->shared->tv->stats);
}
SCReturnInt(loop_result);
}
/** \internal
* \brief get the timestamp of the first packet and rewind
* \param pfv pcap file variables for storing the timestamp
* \retval bool true on success, false on error
*/
static bool PeekFirstPacketTimestamp(PcapFileFileVars *pfv)
{
int r = pcap_next_ex(pfv->pcap_handle, &pfv->first_pkt_hdr, &pfv->first_pkt_data);
if (r <= 0 || pfv->first_pkt_hdr == NULL) {
SCLogError("failed to get first packet timestamp. pcap_next_ex(): %d", r);
return false;
}
/* timestamp in pfv->first_pkt_hdr may not be 'struct timeval' so
* do a manual copy of the members. */
pfv->first_pkt_ts.tv_sec = pfv->first_pkt_hdr->ts.tv_sec;
pfv->first_pkt_ts.tv_usec = pfv->first_pkt_hdr->ts.tv_usec;
return true;
}
TmEcode InitPcapFile(PcapFileFileVars *pfv)
{
char errbuf[PCAP_ERRBUF_SIZE] = "";
if(unlikely(pfv->filename == NULL)) {
SCLogError("Filename was null");
SCReturnInt(TM_ECODE_FAILED);
}
pfv->pcap_handle = pcap_open_offline(pfv->filename, errbuf);
if (pfv->pcap_handle == NULL) {
SCLogError("%s", errbuf);
SCReturnInt(TM_ECODE_FAILED);
}
#if defined(HAVE_SETVBUF) && defined(OS_LINUX)
if (pcap_g.read_buffer_size > 0) {
struct stat sb;
int fd = fileno(pcap_file(pfv->pcap_handle));
if (fd >= 0 && fstat(fd, &sb) == 0 && !S_ISREG(sb.st_mode)) {
SCLogInfo("%s: skipping setvbuf, underlying fd is not a regular file", pfv->filename);
} else {
errno = 0;
if (setvbuf(pcap_file(pfv->pcap_handle), pfv->buffer, _IOFBF,
pcap_g.read_buffer_size) != 0) {
SCLogWarning("Failed to setvbuf on PCAP file handle: %s", strerror(errno));
}
}
}
#endif
if (pfv->shared != NULL && pfv->shared->bpf_string != NULL) {
SCLogInfo("using bpf-filter \"%s\"", pfv->shared->bpf_string);
if (pcap_compile(pfv->pcap_handle, &pfv->filter, pfv->shared->bpf_string, 1, 0) < 0) {
SCLogError("bpf compilation error %s for %s", pcap_geterr(pfv->pcap_handle),
pfv->filename);
SCReturnInt(TM_ECODE_FAILED);
}
if (pcap_setfilter(pfv->pcap_handle, &pfv->filter) < 0) {
SCLogError("could not set bpf filter %s for %s", pcap_geterr(pfv->pcap_handle),
pfv->filename);
pcap_freecode(&pfv->filter);
SCReturnInt(TM_ECODE_FAILED);
}
pcap_freecode(&pfv->filter);
}
SC_ATOMIC_INIT(pfv->alerts_count);
SC_ATOMIC_SET(pfv->alerts_count, 0);
SC_ATOMIC_INIT(pfv->ref_cnt);
SC_ATOMIC_SET(pfv->ref_cnt, 0);
pfv->cleanup_requested = false;
pfv->datalink = pcap_datalink(pfv->pcap_handle);
SCLogDebug("datalink %" PRId32 "", pfv->datalink);
DatalinkSetGlobalType(pfv->datalink);
if (!PeekFirstPacketTimestamp(pfv))
SCReturnInt(TM_ECODE_FAILED);
DecoderFunc UnusedFnPtr;
TmEcode validated = ValidateLinkType(pfv->datalink, &UnusedFnPtr);
SCReturnInt(validated);
}
TmEcode ValidateLinkType(int datalink, DecoderFunc *DecoderFn)
{
switch (datalink) {
case LINKTYPE_LINUX_SLL2:
*DecoderFn = DecodeSll2;
break;
case LINKTYPE_LINUX_SLL:
*DecoderFn = DecodeSll;
break;
case LINKTYPE_ETHERNET:
*DecoderFn = DecodeEthernet;
break;
case LINKTYPE_PPP:
*DecoderFn = DecodePPP;
break;
case LINKTYPE_IPV4:
case LINKTYPE_IPV6:
case LINKTYPE_RAW:
case LINKTYPE_RAW2:
case LINKTYPE_GRE_OVER_IP:
*DecoderFn = DecodeRaw;
break;
case LINKTYPE_NULL:
*DecoderFn = DecodeNull;
break;
case LINKTYPE_CISCO_HDLC:
*DecoderFn = DecodeCHDLC;
break;
default:
SCLogError(
"datalink type %" PRId32 " not (yet) supported in module PcapFile.", datalink);
SCReturnInt(TM_ECODE_FAILED);
}
SCReturnInt(TM_ECODE_OK);
}
bool PcapFileShouldDeletePcapFile(PcapFileFileVars *pfv)
{
if (pfv == NULL || pfv->shared == NULL) {
return false;
}
if (pfv->shared->delete_mode == PCAP_FILE_DELETE_NONE) {
return false;
}
if (pfv->shared->delete_mode == PCAP_FILE_DELETE_ALWAYS) {
return true;
}
/* PCAP_FILE_DELETE_NON_ALERTS mode */
uint64_t file_alerts = SC_ATOMIC_GET(pfv->alerts_count);
if (file_alerts != 0) {
SCLogDebug("Skipping deletion of %s due to %" PRIu64 " alert(s) generated.", pfv->filename,
file_alerts);
return false;
}
SCLogDebug("pcap-file: will delete %s (no alerts counted)", pfv->filename);
return true;
}
void PcapFileFinalizePacket(PcapFileFileVars *pfv)
{
if (pfv != NULL) {
/* decrease ref count as packet is done */
uint32_t prev = SC_ATOMIC_SUB(pfv->ref_cnt, 1);
SCLogDebug("pcap-file: packet done pfv=%p filename=%s ref_cnt was=%u now=%u", (void *)pfv,
pfv->filename, prev, prev - 1);
if (prev == 1) {
if (pfv->cleanup_requested) {
CleanupPcapFileFileVars(pfv);
}
}
}
}
void PcapFileAddAlertCount(PcapFileFileVars *pfv, uint16_t alert_count)
{
if (pfv != NULL && alert_count > 0) {
SC_ATOMIC_ADD(pfv->alerts_count, alert_count);
}
}
static void PcapCaptureOnPacketWithAlerts(const Packet *p)
{
PcapFileFileVars *pfv = p->pcap_v.pfv;
if (pfv == NULL) {
pfv = PcapFileGetCurrentPfv();
}
if (pfv != NULL) {
/* alerts.cnt is uint16_t; count alerts for delete-on-non-alerts logic */
PcapFileAddAlertCount(pfv, p->alerts.cnt);
}
}
static void PcapCaptureOnPseudoPacketCreated(Packet *p)
{
/* For pseudo packets created by generic layers, associate with current pfv
* and ensure refcount held so deletion defers. */
if (p->pcap_v.pfv == NULL) {
PcapFileFileVars *pfv = PcapFileGetCurrentPfv();
if (pfv != NULL) {
p->pcap_v.pfv = pfv;
p->ReleasePacket = PcapFileReleasePseudoPacket;
SC_ATOMIC_ADD(pfv->ref_cnt, 1);
}
}
}
void PcapFileInstallCaptureHooks(void)
{
CaptureHooksSet(PcapCaptureOnPacketWithAlerts, PcapCaptureOnPseudoPacketCreated);
}
PcapFileDeleteMode PcapFileParseDeleteMode(void)
{
PcapFileDeleteMode delete_mode = PCAP_FILE_DELETE_NONE;
const char *delete_when_done_str = NULL;
if (SCConfGetNonNull("pcap-file.delete-when-done", &delete_when_done_str) == 1) {
if (strcmp(delete_when_done_str, "non-alerts") == 0) {
delete_mode = PCAP_FILE_DELETE_NON_ALERTS;
} else {
int delete_always = 0;
if (SCConfGetBool("pcap-file.delete-when-done", &delete_always) == 1) {
if (delete_always == 1) {
delete_mode = PCAP_FILE_DELETE_ALWAYS;
}
}
}
}
return delete_mode;
}
#ifdef UNITTESTS
#include "util-unittest-helper.h"
/**
* \test Tests that the PcapFileShouldDeletePcapFile function correctly applies the
* delete mode configuration.
*/
static int SourcePcapFileHelperTest01(void)
{
PcapFileSharedVars shared;
memset(&shared, 0, sizeof(shared));
shared.delete_mode = PCAP_FILE_DELETE_ALWAYS;
PcapFileFileVars pfv;
memset(&pfv, 0, sizeof(pfv));
pfv.shared = &shared;
pfv.filename = SCStrdup("test.pcap");
SC_ATOMIC_INIT(pfv.alerts_count);
SC_ATOMIC_SET(pfv.alerts_count, 0);
/* Test case 1: Always delete mode */
int result1 = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF_NOT(result1);
/* Test case 2: Non-alerts mode with no alerts */
shared.delete_mode = PCAP_FILE_DELETE_NON_ALERTS;
int result2 = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF_NOT(result2);
/* Test case 3: Non-alerts mode with alerts */
SC_ATOMIC_ADD(pfv.alerts_count, 1);
int result3 = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF(result3);
/* Test case 4: Always delete mode with alerts */
shared.delete_mode = PCAP_FILE_DELETE_ALWAYS;
int result4 = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF_NOT(result4);
/* Test case 5: No delete mode */
shared.delete_mode = PCAP_FILE_DELETE_NONE;
int result5 = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF(result5);
SCFree(pfv.filename);
PASS;
}
/**
* \test Test PcapFileFinalizePacket function with reference counting
*/
static int SourcePcapFileHelperTest02(void)
{
PcapFileFileVars pfv;
memset(&pfv, 0, sizeof(pfv));
SC_ATOMIC_INIT(pfv.alerts_count);
SC_ATOMIC_SET(pfv.alerts_count, 0);
SC_ATOMIC_INIT(pfv.ref_cnt);
SC_ATOMIC_SET(pfv.ref_cnt, 0);
pfv.cleanup_requested = false;
/* Test adding alerts with reference counting */
SC_ATOMIC_ADD(pfv.ref_cnt, 1); /* simulate packet in flight */
PcapFileAddAlertCount(&pfv, 5);
PcapFileFinalizePacket(&pfv);
uint64_t count = SC_ATOMIC_GET(pfv.alerts_count);
FAIL_IF_NOT(count == 5);
FAIL_IF_NOT(SC_ATOMIC_GET(pfv.ref_cnt) == 0); /* should be decremented */
/* Test adding more alerts */
SC_ATOMIC_ADD(pfv.ref_cnt, 1);
PcapFileAddAlertCount(&pfv, 3);
PcapFileFinalizePacket(&pfv);
count = SC_ATOMIC_GET(pfv.alerts_count);
FAIL_IF_NOT(count == 8);
/* Test with zero alerts (should not increment count) */
SC_ATOMIC_ADD(pfv.ref_cnt, 1);
PcapFileFinalizePacket(&pfv);
count = SC_ATOMIC_GET(pfv.alerts_count);
FAIL_IF_NOT(count == 8);
/* Test with NULL pfv (should not crash) */
PcapFileFinalizePacket(NULL);
PASS;
}
/* Mock for configuration testing */
static int SetupYamlConf(const char *conf_string)
{
SCConfCreateContextBackup();
SCConfInit();
return SCConfYamlLoadString(conf_string, strlen(conf_string));
}
static void CleanupYamlConf(void)
{
SCConfDeInit();
SCConfRestoreContextBackup();
}
/**
* \test Test PcapFileParseDeleteMode with all configuration combinations
*/
static int SourcePcapFileHelperTest03(void)
{
/* Test 1: No configuration (should default to NONE) */
SCConfCreateContextBackup();
SCConfInit();
PcapFileDeleteMode result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_NONE);
SCConfDeInit();
SCConfRestoreContextBackup();
/* Test 2: "false" configuration */
const char *conf_false = "%YAML 1.1\n"
"---\n"
"pcap-file:\n"
" delete-when-done: false\n";
SetupYamlConf(conf_false);
result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_NONE);
CleanupYamlConf();
/* Test 3: "true" configuration */
const char *conf_true = "%YAML 1.1\n"
"---\n"
"pcap-file:\n"
" delete-when-done: true\n";
SetupYamlConf(conf_true);
result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS);
CleanupYamlConf();
/* Test 4: "non-alerts" configuration */
const char *conf_non_alerts = "%YAML 1.1\n"
"---\n"
"pcap-file:\n"
" delete-when-done: \"non-alerts\"\n";
SetupYamlConf(conf_non_alerts);
result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_NON_ALERTS);
CleanupYamlConf();
/* Test 5: Invalid configuration (should default to NONE) */
const char *conf_invalid = "%YAML 1.1\n"
"---\n"
"pcap-file:\n"
" delete-when-done: \"invalid-value\"\n";
SetupYamlConf(conf_invalid);
result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_NONE);
CleanupYamlConf();
PASS;
}
/**
* \test pfv is NULL.
*/
static int SourcePcapFileHelperTest04(void)
{
int rc = PcapFileShouldDeletePcapFile(NULL);
FAIL_IF(rc);
PASS;
}
/**
* \test pfv->shared is NULL.
*/
static int SourcePcapFileHelperTest05(void)
{
PcapFileFileVars pfv;
memset(&pfv, 0, sizeof(pfv));
int rc = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF(rc);
PASS;
}
/**
* \test Test cleanup with reference counting and deferred deletion
*/
static int SourcePcapFileHelperTest06(void)
{
PcapFileFileVars pfv;
memset(&pfv, 0, sizeof(pfv));
SC_ATOMIC_INIT(pfv.alerts_count);
SC_ATOMIC_SET(pfv.alerts_count, 0);
SC_ATOMIC_INIT(pfv.ref_cnt);
SC_ATOMIC_SET(pfv.ref_cnt, 2); /* simulate 2 packets in flight */
pfv.cleanup_requested = false;
/* Simulate first packet completion - should not cleanup yet */
SC_ATOMIC_SUB(pfv.ref_cnt, 1);
FAIL_IF_NOT(SC_ATOMIC_GET(pfv.ref_cnt) == 1);
/* Request cleanup while packets are still in flight */
pfv.cleanup_requested = true;
/* Simulate second packet completion - should trigger cleanup */
if (SC_ATOMIC_SUB(pfv.ref_cnt, 1) == 1) {
FAIL_IF_NOT(pfv.cleanup_requested); /* cleanup should have been requested */
/* In real code, CleanupPcapFileFileVars would be called here */
}
PASS;
}
/**
* \test Test edge cases and error conditions
*/
static int SourcePcapFileHelperTest07(void)
{
/* Test 1: PcapFileShouldDeletePcapFile with very high alert count */
PcapFileSharedVars shared;
memset(&shared, 0, sizeof(shared));
shared.delete_mode = PCAP_FILE_DELETE_NON_ALERTS;
PcapFileFileVars pfv;
memset(&pfv, 0, sizeof(pfv));
pfv.shared = &shared;
pfv.filename = SCStrdup("test.pcap");
SC_ATOMIC_INIT(pfv.alerts_count);
SC_ATOMIC_SET(pfv.alerts_count, UINT64_MAX); /* max value */
int result = PcapFileShouldDeletePcapFile(&pfv);
FAIL_IF(result); /* should not delete with max alerts */
/* Test 2: PcapFileFinalizePacket with max alert count */
SC_ATOMIC_INIT(pfv.ref_cnt);
SC_ATOMIC_ADD(pfv.ref_cnt, 1);
PcapFileAddAlertCount(&pfv, UINT16_MAX); /* max uint16_t */
PcapFileFinalizePacket(&pfv);
/* Should not overflow or crash */
SCFree(pfv.filename);
PASS;
}
/**
* \test Test command-line --pcap-file-delete override behavior
*/
static int SourcePcapFileHelperTest08(void)
{
/* Test 1: Command line overrides YAML "false" */
const char *conf_false = "%YAML 1.1\n"
"---\n"
"pcap-file:\n"
" delete-when-done: false\n";
SetupYamlConf(conf_false);
/* Simulate --pcap-file-delete command line option */
int set_result = SCConfSetFinal("pcap-file.delete-when-done", "true");
FAIL_IF_NOT(set_result == 1);
PcapFileDeleteMode result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); /* Should override YAML false */
CleanupYamlConf();
/* Test 2: Command line overrides YAML "non-alerts" */
const char *conf_non_alerts = "%YAML 1.1\n"
"---\n"
"pcap-file:\n"
" delete-when-done: \"non-alerts\"\n";
SetupYamlConf(conf_non_alerts);
/* Simulate --pcap-file-delete command line option */
set_result = SCConfSetFinal("pcap-file.delete-when-done", "true");
FAIL_IF_NOT(set_result == 1);
result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); /* Should override YAML "non-alerts" */
CleanupYamlConf();
/* Test 3: Command line overrides no YAML config */
SCConfCreateContextBackup();
SCConfInit();
/* Simulate --pcap-file-delete command line option with no YAML config */
set_result = SCConfSetFinal("pcap-file.delete-when-done", "true");
FAIL_IF_NOT(set_result == 1);
result = PcapFileParseDeleteMode();
FAIL_IF_NOT(result == PCAP_FILE_DELETE_ALWAYS); /* Should set to always delete */
SCConfDeInit();
SCConfRestoreContextBackup();
PASS;
}
/**
* \test Test that cleanup defers while packets are in flight and that a file
* with alerts is not deleted in NON_ALERTS mode.
*/
static int SourcePcapFileHelperTest09(void)
{
PcapFileSharedVars *shared = SCCalloc(1, sizeof(*shared));
FAIL_IF_NULL(shared);
shared->delete_mode = PCAP_FILE_DELETE_NON_ALERTS;
PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv));
FAIL_IF_NULL(pfv);
pfv->shared = shared;
pfv->filename = SCStrdup("unit_del_test.pcap");
FAIL_IF_NULL(pfv->filename);
SC_ATOMIC_INIT(pfv->alerts_count);
SC_ATOMIC_SET(pfv->alerts_count, 0);
SC_ATOMIC_INIT(pfv->ref_cnt);
SC_ATOMIC_SET(pfv->ref_cnt, 2); /* two packets in flight */
pfv->cleanup_requested = false;
/* Request cleanup while packets still in flight: should defer. */
CleanupPcapFileFileVars(pfv);
FAIL_IF_NOT(pfv->cleanup_requested);
FAIL_IF_NULL(pfv->filename); /* not freed yet */
/* First packet completes and generates an alert. */
PcapFileAddAlertCount(pfv, 1);
PcapFileFinalizePacket(pfv);
FAIL_IF_NOT(SC_ATOMIC_GET(pfv->alerts_count) == 1);
/* Second (last) packet completes: triggers final cleanup. */
PcapFileFinalizePacket(pfv);
/* pfv memory is freed at this point; only free shared. */
SCFree(shared);
PASS;
}
/**
* \test Cover unlink-on-ALWAYS branch (ref_cnt == 0) and deferred deletion when ref_cnt > 0
*/
static int SourcePcapFileHelperTest10(void)
{
/* Create a temporary file that we expect to be deleted. */
const char *tmpname = "suri_ut_delete_always.pcap";
const uint8_t dummy[] = { 0x00 };
int rc = TestHelperBufferToFile(tmpname, dummy, sizeof(dummy));
FAIL_IF_NOT(rc >= 0);
/* Case 1: delete ALWAYS with no packets in flight -> file unlinked immediately */
PcapFileSharedVars *shared1 = SCCalloc(1, sizeof(*shared1));
FAIL_IF_NULL(shared1);
shared1->delete_mode = PCAP_FILE_DELETE_ALWAYS;
PcapFileFileVars *pfv1 = SCCalloc(1, sizeof(*pfv1));
FAIL_IF_NULL(pfv1);
pfv1->shared = shared1;
pfv1->filename = SCStrdup(tmpname);
FAIL_IF_NULL(pfv1->filename);
SC_ATOMIC_INIT(pfv1->alerts_count);
SC_ATOMIC_SET(pfv1->alerts_count, 0);
SC_ATOMIC_INIT(pfv1->ref_cnt);
SC_ATOMIC_SET(pfv1->ref_cnt, 0);
/* Provide a closable handle to cover close path. */
pfv1->pcap_handle = pcap_open_dead(DLT_EN10MB, 65535);
FAIL_IF_NULL(pfv1->pcap_handle);
CleanupPcapFileFileVars(pfv1);
/* File should be gone. */
FILE *f = fopen(tmpname, "rb");
FAIL_IF_NOT_NULL(f);
if (f != NULL)
fclose(f);
/* Case 2: delete ALWAYS but ref_cnt > 0 -> defer until finalize. */
/* Recreate the file. */
rc = TestHelperBufferToFile(tmpname, dummy, sizeof(dummy));
FAIL_IF_NOT(rc >= 0);
PcapFileSharedVars *shared2 = SCCalloc(1, sizeof(*shared2));
FAIL_IF_NULL(shared2);
shared2->delete_mode = PCAP_FILE_DELETE_ALWAYS;
PcapFileFileVars *pfv2 = SCCalloc(1, sizeof(*pfv2));
FAIL_IF_NULL(pfv2);
pfv2->shared = shared2;
pfv2->filename = SCStrdup(tmpname);
FAIL_IF_NULL(pfv2->filename);
SC_ATOMIC_INIT(pfv2->alerts_count);
SC_ATOMIC_SET(pfv2->alerts_count, 0);
SC_ATOMIC_INIT(pfv2->ref_cnt);
SC_ATOMIC_SET(pfv2->ref_cnt, 1);
pfv2->pcap_handle = pcap_open_dead(DLT_EN10MB, 65535);
FAIL_IF_NULL(pfv2->pcap_handle);
CleanupPcapFileFileVars(pfv2);
/* Still exists now because deletion should be deferred. */
f = fopen(tmpname, "rb");
FAIL_IF_NULL(f);
if (f != NULL)
fclose(f);
/* Finalize the last packet which should trigger final cleanup & unlink. */
PcapFileFinalizePacket(pfv2);
/* Now the file should be gone. */
f = fopen(tmpname, "rb");
FAIL_IF_NOT_NULL(f);
if (f != NULL)
fclose(f);
SCFree(shared1);
SCFree(shared2);
PASS;
}
/**
* \test Test PcapFileReleasePseudoPacket refcount decrement without cleanup
*/
static int SourcePcapFileHelperTest11(void)
{
/* Setup pfv with ref_cnt=2 so release does not trigger cleanup */
PcapFileFileVars pfv;
memset(&pfv, 0, sizeof(pfv));
SC_ATOMIC_INIT(pfv.ref_cnt);
SC_ATOMIC_SET(pfv.ref_cnt, 2);
pfv.cleanup_requested = false;
/* Allocate a packet from the pool to allow safe release */
Packet *p = PacketGetFromAlloc();
FAIL_IF_NULL(p);
p->pcap_v.pfv = &pfv;
/* Call release and ensure ref count decremented by 1 */
PcapFileReleasePseudoPacket(p);
FAIL_IF_NOT(SC_ATOMIC_GET(pfv.ref_cnt) == 1);
PASS;
}
/**
* \test Test adding alert count and that cleanup after refcnt reaches zero
* does not delete when alerts exist in NON_ALERTS mode.
*/
static int SourcePcapFileHelperTest12(void)
{
PcapFileSharedVars shared;
memset(&shared, 0, sizeof(shared));
shared.delete_mode = PCAP_FILE_DELETE_NON_ALERTS;
PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv));
FAIL_IF_NULL(pfv);
pfv->shared = &shared;
pfv->filename = SCStrdup("ut_non_alerts.pcap");
FAIL_IF_NULL(pfv->filename);
SC_ATOMIC_INIT(pfv->alerts_count);
SC_ATOMIC_SET(pfv->alerts_count, 0);
SC_ATOMIC_INIT(pfv->ref_cnt);
SC_ATOMIC_SET(pfv->ref_cnt, 1);
pfv->cleanup_requested = true;
/* Simulate pseudo alert */
PcapFileAddAlertCount(pfv, 2);
FAIL_IF_NOT(SC_ATOMIC_GET(pfv->alerts_count) == 2);
/* Simulate last ref release triggering cleanup; file shouldn't be deleted
* due to alerts > 0. We cannot check unlink here; rely on return value. */
if (SC_ATOMIC_SUB(pfv->ref_cnt, 1) == 1) {
CleanupPcapFileFileVars(pfv);
}
/* Success if no crash. */
PASS;
}
/**
* \test Test global current pfv pointer lifecycle
*/
static int SourcePcapFileHelperTest13(void)
{
PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv));
FAIL_IF_NULL(pfv);
pfv->filename = SCStrdup("ut_global_clear.pcap");
FAIL_IF_NULL(pfv->filename);
PcapFileSetCurrentPfv(pfv);
FAIL_IF_NOT(PcapFileGetCurrentPfv() == pfv);
/* Cleanup should clear global reference when pointing to this pfv */
CleanupPcapFileFileVars(pfv);
/* Global accessor must be NULL after cleanup. */
FAIL_IF_NOT(PcapFileGetCurrentPfv() == NULL);
PASS;
}
/**
* \test Exercise unlink failure branch in CleanupPcapFileFileVars
*/
static int SourcePcapFileHelperTest14(void)
{
PcapFileSharedVars shared;
memset(&shared, 0, sizeof(shared));
shared.delete_mode = PCAP_FILE_DELETE_ALWAYS;
PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv));
FAIL_IF_NULL(pfv);
pfv->shared = &shared;
pfv->filename = SCStrdup("does-not-exist-ut.pcap");
FAIL_IF_NULL(pfv->filename);
SC_ATOMIC_INIT(pfv->ref_cnt);
SC_ATOMIC_SET(pfv->ref_cnt, 0);
/* Attempt cleanup; unlink should fail but must not crash */
CleanupPcapFileFileVars(pfv);
PASS;
}
/**
* \test Cover alerts hook fallback using current PFV
*/
static int SourcePcapFileHelperTest15(void)
{
PcapFileInstallCaptureHooks();
PcapFileFileVars *pfv = SCCalloc(1, sizeof(*pfv));
FAIL_IF_NULL(pfv);
SC_ATOMIC_INIT(pfv->alerts_count);
SC_ATOMIC_SET(pfv->alerts_count, 0);
/* Set current PFV to exercise fallback path */
PcapFileSetCurrentPfv(pfv);
Packet *p = PacketGetFromAlloc();
FAIL_IF_NULL(p);
p->alerts.cnt = 3; /* simulate 3 alerts */
p->pcap_v.pfv = NULL; /* force fallback */
/* Call hook: it should update pfv->alerts_count */
CaptureHooksOnPacketWithAlerts(p);
FAIL_IF_NOT(SC_ATOMIC_GET(pfv->alerts_count) == 3);
/* Cleanup */
PacketFreeOrRelease(p);
CleanupPcapFileFileVars(pfv);
PASS;
}
/**
* \brief Register unit tests for pcap file helper
*/
void SourcePcapFileHelperRegisterTests(void)
{
UtRegisterTest("SourcePcapFileHelperTest01", SourcePcapFileHelperTest01);
UtRegisterTest("SourcePcapFileHelperTest02", SourcePcapFileHelperTest02);
UtRegisterTest("SourcePcapFileHelperTest03", SourcePcapFileHelperTest03);
UtRegisterTest("SourcePcapFileHelperTest04", SourcePcapFileHelperTest04);
UtRegisterTest("SourcePcapFileHelperTest05", SourcePcapFileHelperTest05);
UtRegisterTest("SourcePcapFileHelperTest06", SourcePcapFileHelperTest06);
UtRegisterTest("SourcePcapFileHelperTest07", SourcePcapFileHelperTest07);
UtRegisterTest("SourcePcapFileHelperTest08", SourcePcapFileHelperTest08);
UtRegisterTest("SourcePcapFileHelperTest09", SourcePcapFileHelperTest09);
UtRegisterTest("SourcePcapFileHelperTest10", SourcePcapFileHelperTest10);
UtRegisterTest("SourcePcapFileHelperTest11", SourcePcapFileHelperTest11);
UtRegisterTest("SourcePcapFileHelperTest12", SourcePcapFileHelperTest12);
UtRegisterTest("SourcePcapFileHelperTest13", SourcePcapFileHelperTest13);
UtRegisterTest("SourcePcapFileHelperTest14", SourcePcapFileHelperTest14);
UtRegisterTest("SourcePcapFileHelperTest15", SourcePcapFileHelperTest15);
}
#endif /* UNITTESTS */