diff --git a/src/detect.c b/src/detect.c index de26ec638e..0e4f3cd970 100644 --- a/src/detect.c +++ b/src/detect.c @@ -1322,7 +1322,9 @@ static int DetectRunTxInspectRule(ThreadVars *tv, DetectEngineCtx *de_ctx, /* for a new inspection we inspect pkt header and packet matches */ if (likely(stored_flags == NULL)) { TRACE_SID_TXS(s->id, tx, "first inspect, run packet matches"); - if (DetectRunInspectRuleHeader(p, f, s, s->flags) == false) { + /* the firewall LTE coverage build already ran the same header check for + * this candidate, so skip repeating it */ + if (!can->fw_lte_header_ok && DetectRunInspectRuleHeader(p, f, s, s->flags) == false) { TRACE_SID_TXS(s->id, tx, "DetectRunInspectRuleHeader() no match"); return -1; } @@ -1533,7 +1535,7 @@ static int DetectRunTxInspectRule(ThreadVars *tv, DetectEngineCtx *de_ctx, } else if ((inspect_flags & DE_STATE_FLAG_FULL_INSPECT) == 0 && mpm_in_progress) { TRACE_SID_TXS(s->id, tx, "no need to store no-match sig, " "mpm will revisit it"); - return -1; /* no match */ + return -2; /* no match, but not final: mpm will revisit it */ } else if (inspect_flags != 0 || file_no_match != 0) { TRACE_SID_TXS(s->id, tx, "storing state: flags %08x", inspect_flags); DetectRunStoreStateTx(scratch->sgh, f, tx->tx_ptr, tx->tx_id, s, @@ -1641,7 +1643,7 @@ static inline void StoreDetectProgress( // Merge 'state' rules from the regular prefilter // updates array_idx on the way static inline void RuleMatchCandidateMergeStateRules( - DetectEngineThreadCtx *det_ctx, uint32_t *array_idx) + DetectEngineThreadCtx *det_ctx, uint32_t *array_idx, bool *fw_lte) { // Now, we will merge 2 sorted lists : // the one in det_ctx->tx_candidates @@ -1663,6 +1665,9 @@ static inline void RuleMatchCandidateMergeStateRules( for (uint32_t i = 0; i < det_ctx->match_array_cnt; i++) { const Signature *s = det_ctx->match_array[i]; if (s->app_inspect != NULL) { + if (s->flags & SIG_FLAG_FW_HOOK_LTE) { + *fw_lte = true; + } (*array_idx)++; } } @@ -1695,6 +1700,9 @@ static inline void RuleMatchCandidateMergeStateRules( // take the element from tx_candidates before merge det_ctx->tx_candidates[k].s = det_ctx->tx_candidates[j].s; det_ctx->tx_candidates[k].id = det_ctx->tx_candidates[j].id; + det_ctx->tx_candidates[k].fw_lte_counted = det_ctx->tx_candidates[j].fw_lte_counted; + det_ctx->tx_candidates[k].fw_lte_header_ok = + det_ctx->tx_candidates[j].fw_lte_header_ok; det_ctx->tx_candidates[k].flags = det_ctx->tx_candidates[j].flags; det_ctx->tx_candidates[k].stream_reset = det_ctx->tx_candidates[j].stream_reset; continue; @@ -1705,6 +1713,8 @@ static inline void RuleMatchCandidateMergeStateRules( // take the element from match_array det_ctx->tx_candidates[k].s = s; det_ctx->tx_candidates[k].id = s->iid; + det_ctx->tx_candidates[k].fw_lte_counted = false; + det_ctx->tx_candidates[k].fw_lte_header_ok = false; det_ctx->tx_candidates[k].flags = NULL; det_ctx->tx_candidates[k].stream_reset = 0; } @@ -1720,8 +1730,156 @@ struct DetectFirewallAppTxState { bool fw_last_for_progress; bool fw_next_progress_missing; bool last_fw_rule; /**< processing the last fw rule, so we need to eval all hooks after it. */ + /** det_ctx->fw_lte_cover holds the counts for this walk. The counts answer + * "does another pending rule cover hook H": per hook, how many pending LTE + * rules cover it (their own hook and every prior hook). Progress values + * stay under APP_LAYER_MAX_PROGRESS. */ + bool fw_lte_cover_active; }; +/** \internal + * \brief initialize the per-thread LTE hook coverage for this walk. + * + * The counts live in the thread ctx so IDS traffic does not pay to clear them. + * They are cleared lazily, on the first LTE rule of the walk. + */ +static inline void DetectFwEnsureLteCoverage( + DetectEngineThreadCtx *det_ctx, struct DetectFirewallAppTxState *fw_state) +{ + if (!fw_state->fw_lte_cover_active) { + memset(det_ctx->fw_lte_cover, 0, sizeof(det_ctx->fw_lte_cover)); + fw_state->fw_lte_cover_active = true; + } +} + +/** \internal + * \brief build the per hook coverage of the pending LTE rules. + * + * Counts, per hook, how many pending LTE rules cover it (their own hook and + * every prior hook). The candidate list is sorted, so duplicates - the state + * store and the per-state registrations can both add a rule - are adjacent. + */ +static void DetectFwBuildLteCoverage(DetectEngineThreadCtx *det_ctx, Packet *p, Flow *f, + struct DetectFirewallAppTxState *fw_state, const uint32_t array_idx) +{ + DetectFwEnsureLteCoverage(det_ctx, fw_state); + for (uint32_t i = 0; i < array_idx; i++) { + const Signature *s = det_ctx->tx_candidates[i].s; + /* only firewall rules implement the per-state policy: a threat + * detection rule using the flags & SIG_FLAG_FW_HOOK_LTE) || !(s->flags & SIG_FLAG_FIREWALL)) { + continue; + } + RuleMatchCandidateTx *can = &det_ctx->tx_candidates[i]; + can->fw_lte_counted = false; + const uint32_t *flags = can->flags; + if (flags == NULL) { + if (!DetectRunInspectRuleHeader(p, f, s, s->flags)) { + /* an out of scope rule never covered anything */ + continue; + } + /* the walk inspects this candidate with the same arguments, so it + * can skip repeating the header check */ + can->fw_lte_header_ok = true; + } + if (flags != NULL && + (*flags & (DE_STATE_FLAG_FULL_INSPECT | DE_STATE_FLAG_SIG_CANT_MATCH)) != 0) { + /* already resolved in an earlier pass */ + continue; + } + /* duplicates of the same signature (state store + per-state + * registrations) are adjacent: the candidate list is sorted by id with + * the flagged entry first */ + DEBUG_VALIDATE_BUG_ON( + i > 0 && det_ctx->tx_candidates[i].id < det_ctx->tx_candidates[i - 1].id); + if (i > 0 && det_ctx->tx_candidates[i - 1].s == s) { + continue; + } + if (unlikely(s->app_progress_hook >= APP_LAYER_MAX_PROGRESS)) { + /* defensive: parse rejects this; skip it so the arrays stay in + * bounds and the rule applies its own default policy */ + DEBUG_VALIDATE_BUG_ON(1); + continue; + } + can->fw_lte_counted = true; + for (uint16_t h = 0; h <= s->app_progress_hook; h++) { + det_ctx->fw_lte_cover[h]++; + } + } +} + +/** \internal + * \brief check if a pending LTE rule other than the current one covers its hook. + */ +static inline bool DetectFwOtherLteCoversHook(DetectEngineThreadCtx *det_ctx, + const struct DetectFirewallAppTxState *fw_state, const Signature *s, const bool counted) +{ + if (!fw_state->fw_lte_cover_active) { + /* no LTE candidate in this walk, so nothing covers the hook */ + return false; + } + if (unlikely(s->app_progress_hook >= APP_LAYER_MAX_PROGRESS)) { + DEBUG_VALIDATE_BUG_ON(1); + return false; + } + const uint32_t covered = det_ctx->fw_lte_cover[s->app_progress_hook]; + if (s->flags & SIG_FLAG_FW_HOOK_LTE) { + /* an uncounted candidate (header mismatch, already retired) carries + * no own count in the coverage */ + return covered > (counted ? 1 : 0); + } + return covered > 0; +} + +/** \internal + * \brief count an LTE rule appended mid-walk into the hook coverage. + * + * Appended candidates are the tail of the list, so duplicates cannot be + * detected by adjacency like in DetectFwBuildLteCoverage(). + */ +static inline void DetectFwCountAppendedLteRule(DetectEngineThreadCtx *det_ctx, Packet *p, Flow *f, + struct DetectFirewallAppTxState *fw_state, const Signature *s, const uint32_t can_idx) +{ + if (!(s->flags & SIG_FLAG_FW_HOOK_LTE) || !(s->flags & SIG_FLAG_FIREWALL) || + s->app_progress_hook >= APP_LAYER_MAX_PROGRESS || + !DetectRunInspectRuleHeader(p, f, s, s->flags)) { + return; + } + for (uint32_t k = 0; k < can_idx; k++) { + if (det_ctx->tx_candidates[k].s == s && det_ctx->tx_candidates[k].fw_lte_counted) { + return; + } + } + DetectFwEnsureLteCoverage(det_ctx, fw_state); + for (uint16_t h = 0; h <= s->app_progress_hook; h++) { + det_ctx->fw_lte_cover[h]++; + } + det_ctx->tx_candidates[can_idx].fw_lte_counted = true; + det_ctx->tx_candidates[can_idx].fw_lte_header_ok = true; +} + +/** \internal + * \brief remove an LTE rule that resolved definitively from the hook coverage. + */ +static void DetectFwRetireLteRule(DetectEngineThreadCtx *det_ctx, RuleMatchCandidateTx *can) +{ + const Signature *s = can->s; + if (unlikely(s->app_progress_hook >= APP_LAYER_MAX_PROGRESS)) { + DEBUG_VALIDATE_BUG_ON(1); + return; + } + for (uint16_t h = 0; h <= s->app_progress_hook; h++) { + /* the caller only retires counted candidates: a zero here means the + * accounting is broken, not a legitimate case */ + DEBUG_VALIDATE_BUG_ON(det_ctx->fw_lte_cover[h] == 0); + if (det_ctx->fw_lte_cover[h] > 0) { + det_ctx->fw_lte_cover[h]--; + } + } + /* the candidate no longer contributes to the counts */ + can->fw_lte_counted = false; +} + static inline void DetectRunAppendDefaultAppPolicyAlert(DetectEngineThreadCtx *det_ctx, Packet *p, const bool apply_to_packet, const DetectTransaction *tx, const struct DetectFirewallAppPolicy *ap) @@ -1914,7 +2072,8 @@ static enum DetectTxFirewallFlowControl DetectFirewallApplyDefaultPolicies( */ static enum DetectTxFirewallFlowControl DetectRunTxPreCheckFirewallPolicy( DetectEngineThreadCtx *det_ctx, Packet *p, DetectTransaction *tx, const uint8_t direction, - const Signature *s, const uint32_t can_idx, struct DetectFirewallAppTxState *fw_state) + const Signature *s, const uint32_t can_idx, const bool lte_counted, + struct DetectFirewallAppTxState *fw_state) { SCLogDebug("packet %" PRIu64 ": running pre-checks before sid %u", PcapPacketCntGet(p), s->id); @@ -1963,23 +2122,36 @@ static enum DetectTxFirewallFlowControl DetectRunTxPreCheckFirewallPolicy( SCLogDebug("SIG_FLAG_FW_HOOK_LTE"); return DETECT_TX_FW_FC_OK; // TODO check for other cases } - /* if our first rule is beyond the starting state, we need to check if - * there are rules missing for states in between. */ - if (s->app_progress_hook > tx->detect_progress_orig && can_idx == 0) { - SCLogDebug("missing fw rules at list start: sid %u, progress %u (%u:%u)", s->id, - s->app_progress_hook, tx->detect_progress, tx->detect_progress_orig); - /* if this rule was after the state we expected meaning that there are - * no rules for that state. Invoke the default policies. */ - enum DetectTxFirewallFlowControl r = - DetectFirewallApplyDefaultPolicies(det_ctx, det_ctx->de_ctx->fw_policies, tx, p, - s->alproto, direction, tx->detect_progress_orig, s->app_progress_hook - 1); - if (r != DETECT_TX_FW_FC_OK) { - /* both SKIP and BREAK mean: no more fw rules to inspect. - * SKIP applies to just this TX. - * DROP applies to everything. */ - fw_state->fw_skip_app_filter = true; - } - return r; + + if (can_idx == 0) { + SCLogDebug("check if prior hooks are satisfied: s->app_progress_hook %u, " + "tx->detect_progress_orig %u", + s->app_progress_hook, tx->detect_progress_orig); + + if (DetectFwOtherLteCoversHook(det_ctx, fw_state, s, lte_counted)) { + SCLogDebug("later LTE in-progress rule brought us here: us:%u covered:%u", + s->app_progress_hook, det_ctx->fw_lte_cover[s->app_progress_hook]); + return DETECT_TX_FW_FC_OK; + } + + /* if our first rule is beyond the starting state, we need to check if + * there are rules missing for states in between. */ + if (s->app_progress_hook > tx->detect_progress_orig) { + SCLogDebug("missing fw rules at list start: sid %u, progress %u (%u:%u)", s->id, + s->app_progress_hook, tx->detect_progress, tx->detect_progress_orig); + /* if this rule was after the state we expected meaning that there are + * no rules for that state. Invoke the default policies. */ + enum DetectTxFirewallFlowControl r = DetectFirewallApplyDefaultPolicies(det_ctx, + det_ctx->de_ctx->fw_policies, tx, p, s->alproto, direction, + tx->detect_progress_orig, s->app_progress_hook - 1); + if (r != DETECT_TX_FW_FC_OK) { + /* both SKIP and BREAK mean: no more fw rules to inspect. + * SKIP applies to just this TX. + * DROP applies to everything. */ + fw_state->fw_skip_app_filter = true; + } + return r; + } } return DETECT_TX_FW_FC_OK; } @@ -1996,7 +2168,7 @@ static enum DetectTxFirewallFlowControl DetectRunTxPreCheckFirewallPolicy( */ static enum DetectTxFirewallFlowControl DetectRunTxCheckRuleState(DetectEngineThreadCtx *det_ctx, Packet *p, Flow *f, DetectTransaction *tx, const Signature *s, const uint32_t can_idx, - const uint32_t can_size, struct DetectFirewallAppTxState *fw_state) + const uint32_t can_size, const bool lte_counted, struct DetectFirewallAppTxState *fw_state) { if (s->flags & SIG_FLAG_FIREWALL) { /* check if the next sig is on the same progress hook. If not, we need to apply our @@ -2011,15 +2183,20 @@ static enum DetectTxFirewallFlowControl DetectRunTxCheckRuleState(DetectEngineTh "peek: peeking at sid %u / progress %u", next_s->id, next_s->app_progress_hook); if (next_s->flags & SIG_FLAG_FIREWALL) { if (s->app_progress_hook != next_s->app_progress_hook) { - SCLogDebug("peek: next sid progress %u != current progress %u, so current " - "is last for progress", - next_s->app_progress_hook, s->app_progress_hook); - fw_state->fw_last_for_progress = true; - - if (next_s->app_progress_hook - s->app_progress_hook > 1) { - SCLogDebug("peek: missing progress, so we'll drop that unless we get a " - "sweeping accept first"); - fw_state->fw_next_progress_missing = true; + if (DetectFwOtherLteCoversHook(det_ctx, fw_state, s, lte_counted)) { + SCLogDebug("we have another LTE in-progress. Us:%u covered:%u", + s->app_progress_hook, det_ctx->fw_lte_cover[s->app_progress_hook]); + } else { + SCLogDebug("peek: next sid progress %u != current progress %u, so current " + "is last for progress", + next_s->app_progress_hook, s->app_progress_hook); + fw_state->fw_last_for_progress = true; + + if (next_s->app_progress_hook - s->app_progress_hook > 1) { + SCLogDebug("peek: missing progress, so we'll drop that unless we get a " + "sweeping accept first"); + fw_state->fw_next_progress_missing = true; + } } } } else { @@ -2029,12 +2206,14 @@ static enum DetectTxFirewallFlowControl DetectRunTxCheckRuleState(DetectEngineTh } } else { SCLogDebug("peek: no peek beyond last rule"); - if (s->app_progress_hook < tx->tx_progress) { - SCLogDebug("peek: there are no rules to allow the state after this rule"); - fw_state->fw_next_progress_missing = true; + if (!DetectFwOtherLteCoversHook(det_ctx, fw_state, s, lte_counted)) { + if (s->app_progress_hook < tx->tx_progress) { + SCLogDebug("peek: there are no rules to allow the state after this rule"); + fw_state->fw_next_progress_missing = true; + } + fw_state->fw_last_for_progress = true; + fw_state->last_fw_rule = true; } - fw_state->fw_last_for_progress = true; - fw_state->last_fw_rule = true; } if (fw_state->skip_fw_hook == true) { @@ -2428,6 +2607,7 @@ static void DetectRunTx(ThreadVars *tv, det_ctx->p = p; bool do_sort = false; // do we need to sort the tx candidate list? + bool fw_lte_candidates = false; // does the list have a pending LTE rule? uint32_t array_idx = 0; uint32_t total_rules = det_ctx->match_array_cnt; total_rules += (tx.de_state ? tx.de_state->cnt : 0); @@ -2452,7 +2632,12 @@ static void DetectRunTx(ThreadVars *tv, det_ctx->tx_candidates[array_idx].s = s; det_ctx->tx_candidates[array_idx].id = id; det_ctx->tx_candidates[array_idx].flags = NULL; + det_ctx->tx_candidates[array_idx].fw_lte_counted = false; + det_ctx->tx_candidates[array_idx].fw_lte_header_ok = false; det_ctx->tx_candidates[array_idx].stream_reset = 0; + if (s->flags & SIG_FLAG_FW_HOOK_LTE) { + fw_lte_candidates = true; + } array_idx++; } PMQ_RESET(&det_ctx->pmq); @@ -2466,7 +2651,7 @@ static void DetectRunTx(ThreadVars *tv, #ifdef PROFILING uint32_t x = array_idx; #endif - RuleMatchCandidateMergeStateRules(det_ctx, &array_idx); + RuleMatchCandidateMergeStateRules(det_ctx, &array_idx, &fw_lte_candidates); /* merge stored state into results */ if (tx.de_state != NULL) { @@ -2499,14 +2684,21 @@ static void DetectRunTx(ThreadVars *tv, item->flags &= ~(DE_STATE_FLAG_SIG_CANT_MATCH|DE_STATE_FLAG_FULL_INSPECT|DE_STATE_FLAG_FILE_INSPECT); SCLogDebug("rule id %u, post file reset inspect_flags %u", item->sid, item->flags); } - det_ctx->tx_candidates[array_idx].s = de_ctx->sig_array[item->sid]; + + const Signature *ss = de_ctx->sig_array[item->sid]; + det_ctx->tx_candidates[array_idx].s = ss; det_ctx->tx_candidates[array_idx].id = item->sid; det_ctx->tx_candidates[array_idx].flags = &item->flags; + det_ctx->tx_candidates[array_idx].fw_lte_counted = false; + det_ctx->tx_candidates[array_idx].fw_lte_header_ok = false; det_ctx->tx_candidates[array_idx].stream_reset = 0; + if (ss->flags & SIG_FLAG_FW_HOOK_LTE) { + fw_lte_candidates = true; + } array_idx++; } } - do_sort |= (old && old != array_idx); // sort if continue list adds sids + do_sort |= (array_idx > old); // sort if continue list adds sids SCLogDebug("%p/%" PRIu64 " rules added from 'continue' list: %u", tx.tx_ptr, tx.tx_id, array_idx - old); } @@ -2528,14 +2720,13 @@ static void DetectRunTx(ThreadVars *tv, } #endif - struct DetectFirewallAppTxState fw_state = { - false, - false, - 0, - false, - false, - false, - }; + /* Only firewall mode uses the per-tx firewall state. The LTE flag is + * tracked where the candidates are added: the list mixes fw and TD + * rules across tables in iid order, so a scan cannot stop early. */ + struct DetectFirewallAppTxState fw_state = { 0 }; + if (have_fw_rules && fw_lte_candidates) { + DetectFwBuildLteCoverage(det_ctx, p, f, &fw_state, array_idx); + } SCLogDebug("%s: tx_progress %u tx %p have_fw_rules %s array_idx %u detect_progress_orig %u " "cur detect_progress %u", @@ -2567,9 +2758,9 @@ static void DetectRunTx(ThreadVars *tv, tx.detect_progress, tx.detect_progress_orig, s->app_progress_hook); if (have_fw_rules) { - const enum DetectTxFirewallFlowControl fw_r = - DetectRunTxPreCheckFirewallPolicy(det_ctx, p, &tx, - flow_flags & (STREAM_TOSERVER | STREAM_TOCLIENT), s, i, &fw_state); + const enum DetectTxFirewallFlowControl fw_r = DetectRunTxPreCheckFirewallPolicy( + det_ctx, p, &tx, flow_flags & (STREAM_TOSERVER | STREAM_TOCLIENT), s, i, + can->fw_lte_counted, &fw_state); SCLogDebug("fw fw_skip_app_filter:%s skip_fw_hook:%s " "skip_before_progress:%u fw_last_for_progress:%s " "fw_next_progress_missing:%s", @@ -2631,8 +2822,8 @@ static void DetectRunTx(ThreadVars *tv, if (have_fw_rules) { /* check if we should run this rule and update the firewall flow state */ - const enum DetectTxFirewallFlowControl fw_r = - DetectRunTxCheckRuleState(det_ctx, p, f, &tx, s, i, array_idx, &fw_state); + const enum DetectTxFirewallFlowControl fw_r = DetectRunTxCheckRuleState( + det_ctx, p, f, &tx, s, i, array_idx, can->fw_lte_counted, &fw_state); SCLogDebug("fw fw_skip_app_filter:%s skip_fw_hook:%s " "skip_before_progress:%u fw_last_for_progress:%s " "fw_next_progress_missing:%s", @@ -2650,6 +2841,10 @@ static void DetectRunTx(ThreadVars *tv, const int r = DetectRunTxInspectRule(tv, de_ctx, det_ctx, p, f, flow_flags, alstate, &tx, s, inspect_flags, can, scratch); SCLogDebug("s %u r %d", s->id, r); + if (have_fw_rules && r == -1 && (s->flags & SIG_FLAG_FW_HOOK_LTE) && + can->fw_lte_counted && tx.tx_progress >= s->app_progress_hook) { + DetectFwRetireLteRule(det_ctx, can); + } if (r == 1) { /* match */ DetectRunPostMatch(tv, det_ctx, p, s); @@ -2667,14 +2862,16 @@ static void DetectRunTx(ThreadVars *tv, if (DetectRunTxFirewallRulePartialMatch(det_ctx, s, &tx, p) == 1) { break; } - } else { - if (DetectRunTxFirewallRuleNoMatch(det_ctx, s, &tx, &fw_state, p, flow_flags) == - 1) { + } else if (r == -1) { + if ((s->flags & SIG_FLAG_FIREWALL) != 0 && + DetectRunTxFirewallRuleNoMatch(det_ctx, s, &tx, &fw_state, p, flow_flags) == + 1) { return; } - } + } /* r == -2: no match, but not final: the rule is revisited, so a + * hook default now could decide the packet before the revisit can */ DetectVarProcessList(det_ctx, p->flow, p); - RULE_PROFILING_END(det_ctx, s, r, p); + RULE_PROFILING_END(det_ctx, s, r == 1, p); if (det_ctx->post_rule_work_queue.len > 0) { SCLogDebug("%p/%" PRIu64 " post_rule_work_queue len %u", tx.tx_ptr, tx.tx_id, @@ -2696,8 +2893,16 @@ static void DetectRunTx(ThreadVars *tv, const SigIntId id = ts->iid; det_ctx->tx_candidates[array_idx].s = ts; det_ctx->tx_candidates[array_idx].id = id; + det_ctx->tx_candidates[array_idx].fw_lte_counted = false; + det_ctx->tx_candidates[array_idx].fw_lte_header_ok = false; det_ctx->tx_candidates[array_idx].flags = NULL; det_ctx->tx_candidates[array_idx].stream_reset = 0; + + /* a mid-walk enabled LTE rule must count as coverage + * for the remaining checks of this walk */ + if (have_fw_rules) { + DetectFwCountAppendedLteRule(det_ctx, p, f, &fw_state, ts, array_idx); + } array_idx++; SCLogDebug("%p/%" PRIu64 " rule %u (%u) added from 'post match' prefilter", @@ -2826,6 +3031,8 @@ static void DetectRunFrames(ThreadVars *tv, DetectEngineCtx *de_ctx, DetectEngin det_ctx->tx_candidates[array_idx].s = s; det_ctx->tx_candidates[array_idx].id = id; det_ctx->tx_candidates[array_idx].flags = NULL; + det_ctx->tx_candidates[array_idx].fw_lte_counted = false; + det_ctx->tx_candidates[array_idx].fw_lte_header_ok = false; det_ctx->tx_candidates[array_idx].stream_reset = 0; array_idx++; } @@ -2840,6 +3047,8 @@ static void DetectRunFrames(ThreadVars *tv, DetectEngineCtx *de_ctx, DetectEngin det_ctx->tx_candidates[array_idx].s = s; det_ctx->tx_candidates[array_idx].id = id; det_ctx->tx_candidates[array_idx].flags = NULL; + det_ctx->tx_candidates[array_idx].fw_lte_counted = false; + det_ctx->tx_candidates[array_idx].fw_lte_header_ok = false; det_ctx->tx_candidates[array_idx].stream_reset = 0; array_idx++; @@ -2897,7 +3106,7 @@ static void DetectRunFrames(ThreadVars *tv, DetectEngineCtx *de_ctx, DetectEngin } } DetectVarProcessList(det_ctx, p->flow, p); - RULE_PROFILING_END(det_ctx, s, r, p); + RULE_PROFILING_END(det_ctx, s, r == 1, p); } /* update Frame::inspect_progress here instead of in the code above. The reason is that a diff --git a/src/detect.h b/src/detect.h index 030a6a0e05..bc679e6755 100644 --- a/src/detect.h +++ b/src/detect.h @@ -27,6 +27,7 @@ #include "suricata-common.h" #include "flow.h" +#include "app-layer-parser.h" #include "detect-engine-proto.h" #include "detect-reference.h" #include "detect-metadata.h" @@ -1258,8 +1259,10 @@ typedef struct SignatureNonPrefilterStore_ { /** array of TX inspect rule candidates */ typedef struct RuleMatchCandidateTx { - SigIntId id; /**< internal signature id */ - uint32_t *flags; /**< inspect flags ptr */ + SigIntId id; /**< internal signature id */ + bool fw_lte_counted; /**< counted in the firewall LTE hook coverage */ + bool fw_lte_header_ok; /**< firewall LTE coverage already passed the rule header check */ + uint32_t *flags; /**< inspect flags ptr */ union { struct { bool stream_stored; @@ -1407,6 +1410,11 @@ typedef struct DetectEngineThreadCtx_ { RuleMatchCandidateTx *tx_candidates; uint32_t tx_candidates_size; + /** Per hook LTE coverage of the current tx walk. Kept here instead of in + * the per-tx firewall state so IDS traffic does not pay to clear it. + * Cleared by the firewall coverage build on first use in a walk. */ + uint32_t fw_lte_cover[APP_LAYER_MAX_PROGRESS]; + MpmThreadCtx mtc; /**< thread ctx for the mpm */ /* work queue for post-rule matching affecting prefilter */ PostRuleMatchWorkQueue post_rule_work_queue; diff --git a/src/tests/detect.c b/src/tests/detect.c index 1095fc3163..f0d87f4770 100644 --- a/src/tests/detect.c +++ b/src/tests/detect.c @@ -4629,11 +4629,191 @@ static int DetectAddressYamlParsing04 (void) PASS; } +/** \internal + * \brief fill a synthetic signature with the fields the LTE coverage helpers read. + */ +static void DetectFwLteCoverageTestInitSig( + Signature *s, const uint32_t id, const uint8_t hook, const bool lte, const bool firewall) +{ + memset(s, 0, sizeof(*s)); + s->id = id; + s->iid = (SigIntId)id; + s->app_progress_hook = hook; + s->flags = SIG_FLAG_SRC_ANY | SIG_FLAG_DST_ANY | SIG_FLAG_SP_ANY | SIG_FLAG_DP_ANY; + if (lte) { + s->flags |= SIG_FLAG_FW_HOOK_LTE; + } + if (firewall) { + s->flags |= SIG_FLAG_FIREWALL; + } +} + +/** \test DetectFwBuildLteCoverage/DetectFwOtherLteCoversHook on synthetic rules */ +static int DetectFwLteCoverageTest01(void) +{ + struct DetectFirewallAppTxState fw_state = { 0 }; + DetectEngineThreadCtx det_ctx = { 0 }; + RuleMatchCandidateTx candidates[4] = { 0 }; + det_ctx.tx_candidates = candidates; + + Packet p = { 0 }; + Flow f = { 0 }; + + Signature lte_a, lte_b, non_lte, td_lte; + DetectFwLteCoverageTestInitSig(<e_a, 1, 3, true, true); + DetectFwLteCoverageTestInitSig(<e_b, 2, 3, true, true); + DetectFwLteCoverageTestInitSig(&non_lte, 3, 2, false, true); + /* threat detection rule with a lower-bound hook: not firewall coverage */ + DetectFwLteCoverageTestInitSig(&td_lte, 4, 2, true, false); + + candidates[0].s = <e_a; + candidates[0].id = lte_a.iid; + candidates[1].s = <e_b; + candidates[1].id = lte_b.iid; + candidates[2].s = &non_lte; + candidates[2].id = non_lte.iid; + candidates[3].s = &td_lte; + candidates[3].id = td_lte.iid; + + DetectFwBuildLteCoverage(&det_ctx, &p, &f, &fw_state, 4); + + /* both LTE rules cover their own hook and every prior hook */ + for (uint16_t h = 0; h <= 3; h++) { + FAIL_IF(det_ctx.fw_lte_cover[h] != 2); + } + FAIL_IF(det_ctx.fw_lte_cover[4] != 0); + FAIL_IF_NOT(candidates[0].fw_lte_counted); + FAIL_IF_NOT(candidates[1].fw_lte_counted); + FAIL_IF(candidates[2].fw_lte_counted); + FAIL_IF(candidates[3].fw_lte_counted); + + /* a non-LTE rule is covered by any pending LTE rule; a counted LTE rule + * needs a coverage greater than itself (covered > 1) */ + FAIL_IF_NOT(DetectFwOtherLteCoversHook(&det_ctx, &fw_state, &non_lte, false)); + FAIL_IF_NOT(DetectFwOtherLteCoversHook(&det_ctx, &fw_state, <e_a, true)); + + /* an uncounted LTE candidate carries no own count: the other rule alone + * already covers the hook */ + FAIL_IF_NOT(DetectFwOtherLteCoversHook(&det_ctx, &fw_state, <e_a, false)); + + /* a hook covered by exactly one other rule: counted needs 2, uncounted + * is already covered */ + struct DetectFirewallAppTxState one = { 0 }; + one.fw_lte_cover_active = true; + memset(det_ctx.fw_lte_cover, 0, sizeof(det_ctx.fw_lte_cover)); + det_ctx.fw_lte_cover[3] = 1; /* lte_a's hook */ + FAIL_IF(DetectFwOtherLteCoversHook(&det_ctx, &one, <e_a, true)); + FAIL_IF_NOT(DetectFwOtherLteCoversHook(&det_ctx, &one, <e_a, false)); + + /* out-of-range hook is rejected defensively without crashing */ + Signature out_of_range; + DetectFwLteCoverageTestInitSig(&out_of_range, 4, APP_LAYER_MAX_PROGRESS, true, true); + FAIL_IF(DetectFwOtherLteCoversHook(&det_ctx, &fw_state, &out_of_range, true)); + + PASS; +} + +/** \test DetectFwRetireLteRule decrements only its own hook range */ +static int DetectFwLteCoverageTest02(void) +{ + struct DetectFirewallAppTxState fw_state = { 0 }; + DetectEngineThreadCtx det_ctx = { 0 }; + RuleMatchCandidateTx candidates[2] = { 0 }; + det_ctx.tx_candidates = candidates; + + Packet p = { 0 }; + Flow f = { 0 }; + + Signature lte_a, lte_b; + DetectFwLteCoverageTestInitSig(<e_a, 1, 1, true, true); + DetectFwLteCoverageTestInitSig(<e_b, 2, 4, true, true); + + candidates[0].s = <e_a; + candidates[0].id = lte_a.iid; + candidates[1].s = <e_b; + candidates[1].id = lte_b.iid; + + DetectFwBuildLteCoverage(&det_ctx, &p, &f, &fw_state, 2); + FAIL_IF(det_ctx.fw_lte_cover[0] != 2); + FAIL_IF(det_ctx.fw_lte_cover[1] != 2); + FAIL_IF(det_ctx.fw_lte_cover[2] != 1); + FAIL_IF(det_ctx.fw_lte_cover[4] != 1); + FAIL_IF(det_ctx.fw_lte_cover[5] != 0); + + /* retiring the short rule removes only its contribution */ + DetectFwRetireLteRule(&det_ctx, &candidates[0]); + FAIL_IF(det_ctx.fw_lte_cover[0] != 1); + FAIL_IF(det_ctx.fw_lte_cover[1] != 1); + FAIL_IF(det_ctx.fw_lte_cover[2] != 1); + FAIL_IF(det_ctx.fw_lte_cover[3] != 1); + FAIL_IF(det_ctx.fw_lte_cover[4] != 1); + /* the retired rule no longer counts as its own coverage: it is covered + * by the still pending rule, while the stale counted state would read + * the single remaining count as its own */ + FAIL_IF(candidates[0].fw_lte_counted); + FAIL_IF_NOT( + DetectFwOtherLteCoversHook(&det_ctx, &fw_state, <e_a, candidates[0].fw_lte_counted)); + FAIL_IF(DetectFwOtherLteCoversHook(&det_ctx, &fw_state, <e_a, true)); + + DetectFwRetireLteRule(&det_ctx, &candidates[1]); + for (uint16_t h = 0; h <= 4; h++) { + FAIL_IF(det_ctx.fw_lte_cover[h] != 0); + } + + PASS; +} + +/** \test DetectFwCountAppendedLteRule dedup and counting */ +static int DetectFwLteCoverageTest03(void) +{ + struct DetectFirewallAppTxState fw_state = { 0 }; + DetectEngineThreadCtx det_ctx = { 0 }; + RuleMatchCandidateTx candidates[2] = { 0 }; + det_ctx.tx_candidates = candidates; + + Packet p = { 0 }; + Flow f = { 0 }; + + Signature lte_a, lte_c, non_lte; + DetectFwLteCoverageTestInitSig(<e_a, 1, 1, true, true); + DetectFwLteCoverageTestInitSig(<e_c, 3, 2, true, true); + DetectFwLteCoverageTestInitSig(&non_lte, 4, 1, false, true); + + /* the rule is already counted earlier in the list: must not count again */ + candidates[0].s = <e_a; + candidates[0].id = lte_a.iid; + candidates[0].fw_lte_counted = true; + DetectFwCountAppendedLteRule(&det_ctx, &p, &f, &fw_state, <e_a, 1); + FAIL_IF(det_ctx.fw_lte_cover[0] != 0); + FAIL_IF(det_ctx.fw_lte_cover[1] != 0); + FAIL_IF(candidates[1].fw_lte_counted); + + /* a non-LTE rule does not increase coverage */ + DetectFwCountAppendedLteRule(&det_ctx, &p, &f, &fw_state, &non_lte, 1); + FAIL_IF(det_ctx.fw_lte_cover[0] != 0); + + /* a new LTE rule covers its own hook and every prior hook */ + candidates[1].s = <e_c; + candidates[1].id = lte_c.iid; + DetectFwCountAppendedLteRule(&det_ctx, &p, &f, &fw_state, <e_c, 1); + FAIL_IF(det_ctx.fw_lte_cover[0] != 1); + FAIL_IF(det_ctx.fw_lte_cover[1] != 1); + FAIL_IF(det_ctx.fw_lte_cover[2] != 1); + FAIL_IF(det_ctx.fw_lte_cover[3] != 0); + FAIL_IF_NOT(candidates[1].fw_lte_counted); + + PASS; +} + void SigRegisterTests(void) { SigParseRegisterTests(); IPOnlyRegisterTests(); + UtRegisterTest("DetectFwLteCoverageTest01", DetectFwLteCoverageTest01); + UtRegisterTest("DetectFwLteCoverageTest02", DetectFwLteCoverageTest02); + UtRegisterTest("DetectFwLteCoverageTest03", DetectFwLteCoverageTest03); + UtRegisterTest("SigTest01", SigTest01); UtRegisterTest("SigTest02 -- Offset/Depth match", SigTest02); UtRegisterTest("SigTest03 -- offset/depth mismatch", SigTest03);