Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/fmt_build_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Rust format, build and test

on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
Comment on lines +4 to +7

env:
CARGO_TERM_COLOR: always

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Check formatting
run: cargo fmt --check
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
149 changes: 69 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,103 +1,94 @@
# Suricata-autoconfigurer
# Suriconf: configuration assistant for Suricata

An information manual for setting up Suriconf, a configuration assistant for Suricata.
[![License](https://img.shields.io/badge/license-BSD-blue.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.88+-orange.svg)](https://rustup.rs/)
[![Bachelor's Thesis](https://img.shields.io/badge/thesis-completed-success)](https://www.vut.cz/studenti/zav-prace/detail/170986)

---
Suriconf is an automated configuration assistant for [Suricata](https://github.com/OISF/suricata). It analyzes network traffic and system resources to optimize Suricata's configuration through a modular approach. Each module uses mathematical methods and performance metrics to configure specific Suricata components. Testing showed Suriconf v1.0-dev successfully configured Suricata in 80.8% of test cases with [rules](https://community.emergingthreats.net/).

- [Suricata-autoconfigurer](#suricata-autoconfigurer)
- [1. Dependencies](#1-dependencies)
- [2. Suriconf Configuration](#2-suriconf-configuration)
- [2.1 Configuration Overview](#21-configuration-overview)
- [2.2 Modules](#22-modules)
- [2.3 Variables](#23-variables)
- [3. How to Run](#3-how-to-run)
- [4. Expected Output](#4-expected-output)
## Contents

---

## 1. Dependencies

It is necessary to install **rustup**. Installation instructions are available at:
https://rustup.rs/
> [!WARNING]
The Rust compiler version must be higher than 1.88.

The remaining dependencies required by Suriconf are defined in its configuration. These binaries must be installed and their paths provided in the configuration file. The required tools include:

- Suricata (version 9.0.0-dev (d030a9c4e 2026-04-01))
- ethtool (version 5.13)
- ifconfig (net-tools 2.10-alpha)
- ip (ip utility, iproute2-6.8.0, libbpf 0.5.0)

- [Prerequisites](#prerequisites)
- [Rust toolchain](#rust-toolchain)
- [Configuration](#configuration)
- [Configuration overview](#configuration-overview)
- [Modules](#modules)
- [Variables](#variables)
- [Usage](#usage)
- [Output](#output)
---

## 2. Suriconf configuration file
## Prerequisites

### 2. 1. Configuration Overview
### Rust toolchain
1. Install Rustup from [rustup.rs](https://rustup.rs/).
2. Verify your Rust version: `rustc --version`.

- The entire configuration is defined in a YAML file, typically named `suriconf.yaml`.
> [!WARNING]
> Minimum required version of rustc is 1.88 or higher.

- The default Suricata configuration file is specified by the **`suri-configuration`** parameter.
### Required binaries

- The **`log-dir`** parameter defines the directory where Suricata logs are stored.
This directory must have read and write permissions.
The following tools must be installed, and their paths must be accessible and specified in the configuration file.

- The **`preconf-time`** parameter defines the duration of the Suricata preconfiguration run.
> [!WARNING]
The flow threads module requires a minimum of 6 minutes to configure properly.
| Tool | Version |
|------|---------|
| Suricata | 9.0.0-dev (d030a9c4e 2026-04-01) |
| ethtool | 5.13 |
| ifconfig | net-tools 2.10-alpha |
| ip | iproute2-6.8.0, libbpf 0.5.0 |

- The **`analysis`** parameter determines whether Suricata performs multiple runs with different configuration options (dynamic) or whether all decisions are derived from a single preconfiguration run (static).
> [!WARNING]
In version 1.0-dev, only static analysis is supported.
## Configuration

- The **mode** defines whether Suriconf writes changes directly into the configuration file or only provides configuration suggestions:
- suggestion mode: only recommendations are provided
- modify mode: configuration is modified automatically
### Configuration overview

- The **modify mode** includes two submodes:
- ask mode (user confirms changes)
- force mode (all detected changes are applied automatically)
The entire configuration is defined in a YAML file, typically named `suriconf.yaml`.

> [!WARNING]
In version 1.0-dev, only **modify mode with `yaml_change: force`** is supported.
| Parameter | Description |
|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `suri-configuration` | Path to the default Suricata configuration file. |
| `log-dir` | Directory for Suricata logs (requires read/write permissions). |
| `preconf-time` | Duration of the Suricata preconfiguration run. |
| `analysis` | Analysis type: `dynamic` (multiple Suricata runs) or `static` (single Suricata run). |
| `mode` | Output mode: `suggestion` (recommendations only) or `modify` (writes changes to Suricata configuration file).<br>Modify mode with `yaml_change`: `ask` (user confirms each change) or `force` (all detected changes arre applied automatically). |

---
> [!WARNING]
> - Flow threads module requires minimum 6 minutes (`preconf-time`).
> - Version 1.0-dev supports only `static` analysis.
> - Version 1.0-dev supports only `modify` mode with `yaml_change: force`.

### 2. 2. Modules

- The `modules` section defines all available modules.
- Each module can be enabled or disabled using the `enabled` parameter (`true` / `false`).
- If a module is disabled, Suricata uses its default configuration from `suricata.yaml`.
### Modules

> [!WARNING]
When disabling the `cpu_affinity` module, an interface-specific CPU affinity configuration must be defined in `suricata.yaml`.
The `modules` section defines all available modules. Each module can be enabled or disabled using the `enabled` parameter (`true` / `false`).

---
> [!NOTE]
> For disabled modules, Suriconf uses the default configuration from the Suricata configuration file.

### 2. 3. Variables
> [!WARNING]
> When disabling `cpu_affinity` module, you must define interface-specific CPU affinity section in default Suricata configuration file.

- The `variables` section defines runtime and hardware-related settings:
- network interface used by Suricata
- packet capture mode (only AF_PACKET)
- maximum memory usage
- CPU core vector used by Suriconf
### Variables

- The CPU vector defines logical cores intended for Suriconf configuration and later selection for Suricata execution.
The `variables` section defines runtime and hardware settings.

- If `flow_threads` is enabled:
- management threads are taken from the `max_cpu_usage` vector
| Setting | Description |
|----------------------|--------------------------------------------------------|
| `interface` | Network interface used by Suricata. |
| `capture_mode` | Only AF_PACKET (`af_packet`) supported. |
| `max_memory_usage` | Maximum memory usage limit for Suricata configuration. |
| `max_cpu_usage_vec` | Logical cores for Suricata configuration. |

- When `cpu_affinity` is enabled at startup:
- remaining cores from the `max_cpu_usage` vector are assigned to worker threads
- the number of allocated CPU cores corresponds to the number of RX RSS queues of the network interface
**CPU allocation**
- **Management threads** (when `flow_threads` module enabled): taken from `max_cpu_usage_vec` vector.
- **Worker threads** (when `cpu_affinity` module enabled): assigned from remaining cores, limited by RX RSS queue count (fewer queues = fewer cores used).

---
## Usage

## 3. How to run
Isolate CPU cores specified in the `max_cpu_usage_vec` vector to prevent interference from other processes.

One of the highly recommended practices, or even a requirement when running Suriconf, is isolating CPU cores specified in the `max_cpu_usage` vector.
The purpose of this is to prevent Suriconf from producing inaccurate estimates due to interference from other processes consuming CPU resources.
One possible approach is to use the `grubby` kernel parameter. Below is an example of isolating CPU cores 2 to 4, followed by a system reboot:
Isolate cores 2-4 using `grubby`:

```bash
sudo grubby --update-kernel=ALL --args="isolcpus=2-4" && sudo reboot
Expand All @@ -109,19 +100,17 @@ Use the Cargo package manager to run the project in `src` directory:
cargo run
```

To display available options, pass the -h flag after -- (all Suriconf parameters has to be behind --):
To display available options, pass the `-h` flag after `--`:

```bash
cargo run -- -h
```

---

## 4. Expected output
The expected output should be a successful configuration process that creates `suricata_result.yaml` and `nic_setup.sh` (used for configuring the NIC and **must be executed before running Suricata**), both with the same timestamp.

---
> [!NOTE]
> All Suriconf parameters must be passed after `--`.

## 4. Output
The expected output should be a successful configuration process that creates `suricata_result.yaml` and `nic_setup.sh`, both with the same timestamp.

## 5. Bachelor's thesis testing
All testing results are saved in directory `bt_tests_results` and resources used during testing are in directory `bt_tests_resources`. The Suriconf binary is stored in `src`.
> [!IMPORTANT]
> Execute `nic_setup.sh` before running Suricata to apply network interface settings.
2 changes: 1 addition & 1 deletion src/argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub struct Args {
pub enum Commands {

/// Change defaults for Suricata
Suri {
Suricata {
/// Change path to bin
#[clap(short='b', long)]
path_to_bin: Option<PathBuf>,
Expand Down
24 changes: 12 additions & 12 deletions src/cpu_affinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::fs;
use crate::json::CpuThread;
use crate::module::Module;
use crate::structures::{Analysis, Answer, Change, Keys};
use crate::{CPU_MULTIPLIER, PANIC_THRESHOLD, CPU_USAGE};
use crate::{CPU_MULTIPLIER, CPU_USAGE};
use std::process::Command;
use itertools::{izip};
use std::fs::File;
Expand All @@ -27,7 +27,7 @@ pub struct CpuAffinityModule {

impl Module for CpuAffinityModule {

fn new(analysis: &Analysis, debug: bool) -> Self {
fn new(_analysis: &Analysis, debug: bool) -> Self {
let keys = [
Keys::threads_stat,
Keys::wrk_cpu_set,
Expand Down Expand Up @@ -84,7 +84,7 @@ impl Module for CpuAffinityModule {
self.module_disable_offloading(answers, &mut nic_file);
self.module_set_rss(answers, &mut nic_file);
self.module_af_packet_tuning(answers, &mut nic_file);
self.set_af_packet_threads(answers);
self.set_af_packet_threads();
self.module_set_hard_irq(answers, &mut nic_file);

Change::collect_changes(&self.questions)
Expand Down Expand Up @@ -212,7 +212,7 @@ impl CpuAffinityModule {
for i in 0..vector.len() {
let current = vector.get(i).expect("Unable to get u64.");
if i != 0 {
let before = vector.get(i-1).expect("Unable to get u64.");;
let before = vector.get(i-1).expect("Unable to get u64.");
clean_vector.push(*current - *before);
}
else {
Expand Down Expand Up @@ -443,7 +443,7 @@ impl CpuAffinityModule {
self.get_capture_kernel_drops_stat(answers).iter().map(|a| a.value.last().expect("Expected element.")).sum::<u64>()
}

fn get_wrk_cpu_set(&self, answers: &Vec<Answer<'_>>) -> Vec<u64> {
fn get_wrk_cpu_set(&self) -> Vec<u64> {
self.questions
.get(&Keys::wrk_cpu_set).expect("Unable to get wrk_cpu_set.").as_array()
.expect("wrk_cpu_set is not an array").iter()
Expand Down Expand Up @@ -474,7 +474,7 @@ impl CpuAffinityModule {
}
else {
let mut cpu_counter = 0;
let numa_cpus = self.get_numa_node_with_cpus(answers, numa_node);
let numa_cpus = self.get_numa_node_with_cpus(numa_node);
for cpu in &max_cpu_usage_vec {
if cpu_counter == new_workers {
break;
Expand All @@ -498,8 +498,8 @@ impl CpuAffinityModule {
new_wrk_cpu_set
}

fn set_af_packet_threads(&mut self, answers: &Vec<Answer<'_>>) {
let wrk_cpu_set_len = self.get_wrk_cpu_set(answers).len() as u64;
fn set_af_packet_threads(&mut self) {
let wrk_cpu_set_len = self.get_wrk_cpu_set().len() as u64;
*self.questions.get_mut(&Keys::af_packet_interface_threads).expect("Unable to get af_packet_interface_threads") =
Value::Number(wrk_cpu_set_len.into());
}
Expand All @@ -522,7 +522,7 @@ impl CpuAffinityModule {
numa_node
}

fn get_numa_node_with_cpus(&self, answers: &Vec<Answer<'_>>, numa_node: i8) -> Vec<u64> {
fn get_numa_node_with_cpus(&self, numa_node: i8) -> Vec<u64> {
let numa_node_cpus = fs::read_to_string(
format!("/sys/devices/system/node/node{numa_node}/cpulist")
).expect("Failed to read file.");
Expand All @@ -533,20 +533,20 @@ impl CpuAffinityModule {
let interface = self.get_interface_stat(answers);
let ethtool = self.get_ethtool_stat(answers);
let ifconfig = answers.iter().find(|h| h.key == &Keys::ifconfig).and_then(|h| h.value.as_str()).expect("Ifconfig cannot be found.");
let wrk_cpu_set_len = self.get_wrk_cpu_set(answers).len() as u64;
let wrk_cpu_set_len = self.get_wrk_cpu_set().len() as u64;
yaml::af_packet_tuning(interface, wrk_cpu_set_len, ethtool, ifconfig, nic_file);
}

fn module_set_rss(&self, answers: &Vec<Answer<'_>>, nic_file: &mut File) {
let interface = self.get_interface_stat(answers);
let ethtool = self.get_ethtool_stat(answers);
let wrk_cpu_set_len = self.get_wrk_cpu_set(answers).len() as u64;
let wrk_cpu_set_len = self.get_wrk_cpu_set().len() as u64;
_ = yaml::set_rss(interface, wrk_cpu_set_len, ethtool, nic_file);
}

fn module_set_hard_irq(&self, answers: &Vec<Answer<'_>>, nic_file: &mut File) {
let interface = self.get_interface_stat(answers);
let wrk_cpu_set = self.get_wrk_cpu_set(answers);
let wrk_cpu_set = self.get_wrk_cpu_set();
yaml::set_hard_irq(interface, &wrk_cpu_set, nic_file);
}

Expand Down
6 changes: 3 additions & 3 deletions src/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::module::Module;
use std::collections::HashMap;
use serde_json::{Value};
use std::collections::BTreeMap;
use crate::{FLOW_WINDOW, MAX_AVG_RATIO, LOAD_FACTOR, MIN_AVG_RATIO, FLOW_OBJECT, SYNC_AVG, FLOW_BUCKET, FLOW_LOCAL_THREAD_MAX, MULTIPLIER};
use crate::{FLOW_WINDOW, MAX_AVG_RATIO, LOAD_FACTOR, MIN_AVG_RATIO, FLOW_OBJECT, FLOW_BUCKET, FLOW_LOCAL_THREAD_MAX, MULTIPLIER};

#[derive(Debug)]
pub struct FlowModule {
Expand All @@ -30,7 +30,7 @@ pub struct Counter {
}

impl Module for FlowModule {
fn new(analysis: &Analysis, debug: bool) -> Self {
fn new(_analysis: &Analysis, debug: bool) -> Self {
let keys = [
Keys::max_memory_usage,
Keys::threads_stat,
Expand Down Expand Up @@ -211,7 +211,7 @@ impl FlowModule {
for flow in &time.1.hashes {
let v = current_queues.entry(*flow.0).or_insert(0);
*v += *flow.1;
if (*v > counter.current_max) {
if *v > counter.current_max {
counter.current_max = *v;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/flow_threads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-License-Identifier: BSD-3-Clause
This file represents a Flow threads module.
*/

use crate::structures::{Keys, ModuleResult, RobRegression, Analysis, Thread, Answer, Change, Reason};
use crate::structures::{Keys, ModuleResult, RobRegression, Analysis, Thread, Answer, Change};
use crate::module::Module;
use std::collections::{HashMap};
use serde_json::{Value};
Expand All @@ -27,7 +27,7 @@ pub struct RecyclerUp {
}

impl Module for FlowThreadsModule {
fn new(analysis: &Analysis, debug: bool) -> Self {
fn new(_analysis: &Analysis, debug: bool) -> Self {
let keys = [
Keys::flow_recyclers,
Keys::flow_managers,
Expand Down
8 changes: 4 additions & 4 deletions src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn check_emergency(stats: &PathBuf) -> bool {

let last_stat = match get_the_last_one_stats(stats) {
Ok(last_stat) => {last_stat},
Err(e) => {return false;}
Err(_e) => {return false;}
};

match find_emerg_mode_entered(&last_stat) {
Expand Down Expand Up @@ -704,8 +704,8 @@ impl Preconfiguration {
value.get("flow").and_then(|a| a.get("pkts_toclient")).and_then(|f| f.as_u64()).ok_or("Cannot find packets to client.")?;
let bytes_to_client = flow.get("bytes_toclient").and_then(|f| f.as_u64()).ok_or("Cannot find bytes to client.")?;
let bytes_to_server = flow.get("bytes_toserver").and_then(|f| f.as_u64()).ok_or("Cannot find packets bytes to server.")?;
let start = flow.get("start").ok_or("Cannot find start flow value").and_then(|s| self.find_flow_start_end_time(s, true))?;
let end = flow.get("end").ok_or("Cannot find end flow value").and_then(|e| self.find_flow_start_end_time(e, false))?;
let start = flow.get("start").ok_or("Cannot find start flow value").and_then(|s| self.find_flow_start_end_time(s))?;
let end = flow.get("end").ok_or("Cannot find end flow value").and_then(|e| self.find_flow_start_end_time(e))?;
let state = flow.get("state").and_then(|f| f.as_str()).ok_or("Cannot find a flow state.")?.to_string();
let reason = flow.get("reason").and_then(|f| f.as_str()).ok_or("Cannot find a flow reason.")?.to_string();
let proto = value.get("proto").and_then(|f| f.as_str()).ok_or("Cannot find a flow protocol.")?.to_string();
Expand Down Expand Up @@ -736,7 +736,7 @@ impl Preconfiguration {
Ok(())
}

pub fn find_flow_start_end_time(&self, time: &Value, start: bool) -> Result<u64, &'static str> {
pub fn find_flow_start_end_time(&self, time: &Value) -> Result<u64, &'static str> {
let time_str = time.as_str().ok_or("Time is not a string.")?;

let date_time: DateTime<FixedOffset> = time_str.parse().map_err(|_| "Invalid timestamp")?;
Expand Down
Loading
Loading