aboutsummaryrefslogtreecommitdiff
path: root/net/snort3
diff options
context:
space:
mode:
authorEric Fahlgren <ericfahlgren@gmail.com>2023-11-27 08:21:43 -0800
committerRosen Penev <rosenp@gmail.com>2023-12-03 13:53:58 -0800
commitf21dffc2a306ad97cefa03dac8bcee0552da556f (patch)
treeff687532e830989715d5a324fcc02446250626d3 /net/snort3
parent904438be39d2fb80573ab45ce606a20bb4fbe7e1 (diff)
snort3: complete rework
- Add many options to config file. - Move rules and generated snort.lua to /tmp. - Add script for downloading rules. - Add preliminary reporting capabilites. Signed-off-by: Eric Fahlgren <ericfahlgren@gmail.com>
Diffstat (limited to 'net/snort3')
-rw-r--r--net/snort3/Makefile21
-rw-r--r--net/snort3/files/homenet.lua5
-rw-r--r--net/snort3/files/local.lua3
-rw-r--r--net/snort3/files/main.uc263
-rw-r--r--net/snort3/files/nftables.uc18
-rw-r--r--net/snort3/files/snort-mgr260
-rw-r--r--net/snort3/files/snort-rules92
-rw-r--r--net/snort3/files/snort.config75
-rw-r--r--net/snort3/files/snort.init40
-rw-r--r--net/snort3/files/snort.uc126
10 files changed, 888 insertions, 15 deletions
diff --git a/net/snort3/Makefile b/net/snort3/Makefile
index 5f6b50cc8..3f4df0996 100644
--- a/net/snort3/Makefile
+++ b/net/snort3/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=snort3
PKG_VERSION:=3.1.75.0
-PKG_RELEASE:=1
+PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://github.com/snort3/snort3/archive/refs/tags/
@@ -25,7 +25,7 @@ define Package/snort3
SUBMENU:=Firewall
SECTION:=net
CATEGORY:=Network
- DEPENDS:=+libstdcpp +libdaq3 +libdnet +libopenssl +libpcap +libpcre +libpthread +libuuid +zlib +libhwloc +libtirpc @HAS_LUAJIT_ARCH +luajit +libatomic
+ DEPENDS:=+libstdcpp +libdaq3 +libdnet +libopenssl +libpcap +libpcre +libpthread +libuuid +zlib +libhwloc +libtirpc @HAS_LUAJIT_ARCH +luajit +libatomic +kmod-nft-queue
TITLE:=Lightweight Network Intrusion Detection System
URL:=http://www.snort.org/
MENU:=1
@@ -76,6 +76,10 @@ define Package/snort3/install
$(PKG_INSTALL_DIR)/usr/bin/u2{boat,spewfoo} \
$(1)/usr/bin/
+ $(INSTALL_BIN) \
+ ./files/snort-{mgr,rules} \
+ $(1)/usr/bin/
+
$(INSTALL_DIR) $(1)/usr/lib/snort
$(CP) \
$(PKG_INSTALL_DIR)/usr/lib/snort/daq/daq_hext.so \
@@ -90,6 +94,19 @@ define Package/snort3/install
$(PKG_INSTALL_DIR)/usr/include/snort/lua/snort_plugin.lua \
$(1)/usr/share/lua/
+ $(INSTALL_DIR) $(1)/usr/share/snort
+ $(INSTALL_CONF) \
+ ./files/main.uc \
+ $(1)/usr/share/snort/
+
+ $(INSTALL_DIR) $(1)/usr/share/snort/templates
+ $(INSTALL_CONF) \
+ ./files/nftables.uc \
+ $(1)/usr/share/snort/templates/
+ $(INSTALL_CONF) \
+ ./files/snort.uc \
+ $(1)/usr/share/snort/templates/
+
$(INSTALL_DIR) $(1)/etc/snort/{rules,lists,builtin_rules,so_rules}
$(INSTALL_CONF) \
diff --git a/net/snort3/files/homenet.lua b/net/snort3/files/homenet.lua
index 975f70254..91845611d 100644
--- a/net/snort3/files/homenet.lua
+++ b/net/snort3/files/homenet.lua
@@ -1,3 +1,4 @@
+-- Unused when using 'snort-mgr', do not modify without deep understanding.
-- setup HOME_NET below with your IP range/ranges to protect
-HOME_NET = [[ 192.168.1.0/24 10.1.0.1/24 ]]
-EXTERNAL_NET = "!$HOME_NET"
+--HOME_NET = [[ 192.168.1.0/24 10.1.0.0/24 ]]
+--EXTERNAL_NET = "!$HOME_NET"
diff --git a/net/snort3/files/local.lua b/net/snort3/files/local.lua
index c48ffd0c8..8de694131 100644
--- a/net/snort3/files/local.lua
+++ b/net/snort3/files/local.lua
@@ -1,3 +1,6 @@
+-- This file is no longer used if you are using 'snort-mgr' to create the
+-- configuration. It is left as a sample.
+--
-- use ths file to customize any functions defined in /etc/snort/snort.lua
-- switch tap to inline in ips and uncomment the below to run snort in inline mode
diff --git a/net/snort3/files/main.uc b/net/snort3/files/main.uc
new file mode 100644
index 000000000..7db420f33
--- /dev/null
+++ b/net/snort3/files/main.uc
@@ -0,0 +1,263 @@
+{%
+//------------------------------------------------------------------------------
+// Copyright (c) 2023 Eric Fahlgren <eric.fahlgren@gmail.com>
+// SPDX-License-Identifier: GPL-2.0
+//
+// The tables defined using 'config_item' are the source of record for the
+// configuration file, '/etc/config/snort'. If you wish to add new items,
+// do that only in the tables and propagate that use into the templates.
+//
+//------------------------------------------------------------------------------
+
+import { cursor } from 'uci';
+let uci = cursor();
+
+function wrn(fmt, ...args) {
+ if (getenv("QUIET"))
+ exit(1);
+
+ let msg = "ERROR: " + sprintf(fmt, ...args);
+
+ if (getenv("TTY"))
+ warn(`\033[33m${msg}\033[m\n`);
+ else
+ warn(`[!] ${msg}\n`);
+ exit(1);
+}
+
+//------------------------------------------------------------------------------
+
+function config_item(type, values, def) {
+ // If no default value is provided explicity, then values[0] is used as default.
+ if (! type in [ "enum", "range", "path", "str" ]) {
+ wrn(`Invalid item type '${type}', must be one of "enum", "range", "path" or "str".`);
+ return;
+ }
+ if (type == "range" && (length(values) != 2 || values[0] > values[1])) {
+ wrn(`A 'range' type item must have exactly 2 values in ascending order.`);
+ return;
+ }
+ // Maybe check paths for existence???
+
+ return {
+ type: type,
+ values: values,
+ default: def ?? values[0],
+
+ contains: function(value) {
+ // Check if the value is contained in the listed values,
+ // depending on the item type.
+ switch (this.type) {
+ case "enum":
+ return value in this.values;
+ case "range":
+ return value >= this.values[0] && value <= this.values[1];
+ default:
+ return true;
+ }
+ },
+
+ allowed: function() {
+ // Show a pretty version of the possible values, for error messages.
+ switch (this.type) {
+ case "enum":
+ return "one of [" + join(", ", this.values) + "]";
+ case "range":
+ return `${this.values[0]} <= x <= ${this.values[1]}`;
+ case "path":
+ return "a path string";
+ case "str":
+ return "a string";
+ default:
+ return "???";
+ }
+ },
+ }
+};
+
+const snort_config = {
+ enabled: config_item("enum", [ 0, 1 ], 0), // Defaults to off, so that user must configure before first start.
+ manual: config_item("enum", [ 0, 1 ], 1), // Allow user to manually configure, legacy behavior when enabled.
+ oinkcode: config_item("str", [ "" ]), // User subscription oinkcode. Much more in 'snort-rules' script.
+ home_net: config_item("str", [ "" ], "192.168.1.0/24"),
+ external_net: config_item("str", [ "" ], "any"),
+
+ config_dir: config_item("path", [ "/etc/snort" ]), // Location of the base snort configuration files.
+ temp_dir: config_item("path", [ "/var/snort.d" ]), // Location of all transient snort config, including downloaded rules.
+ log_dir: config_item("path", [ "/var/log" ]), // Location of the generated logs, and oh-by-the-way the snort PID file (why?).
+ logging: config_item("enum", [ 0, 1 ], 1),
+ openappid: config_item("enum", [ 0, 1 ], 0),
+
+ mode: config_item("enum", [ "ids", "ips" ]),
+ method: config_item("enum", [ "pcap", "afpacket", "nfq" ]),
+ action: config_item("enum", [ "alert", "block", "drop", "reject" ]),
+ interface: config_item("str", [ uci.get("network", "wan", "device") ]),
+ snaplen: config_item("range", [ 1518, 65535 ]), // int daq.snaplen = 1518: set snap length (same as -s) { 0:65535 }
+};
+
+const nfq_config = {
+ queue_count: config_item("range", [ 1, 16 ], 4), // Count of queues to allocate in nft chain when method=nfq, usually 2-8.
+ queue_start: config_item("range", [ 1, 32768], 4), // Start of queue numbers in nftables.
+ queue_maxlen: config_item("range", [ 1024, 65536 ], 1024), // --daq-var queue_maxlen=int
+ fanout_type: config_item("enum", [ "hash", "lb", "cpu", "rollover", "rnd", "qm"], "hash"), // See below.
+ thread_count: config_item("range", [ 0, 32 ], 0), // 0 = use cpu count
+ chain_type: config_item("enum", [ "prerouting", "input", "forward", "output", "postrouting" ], "input"),
+ chain_priority: config_item("enum", [ "raw", "filter", "300"], "filter"),
+ include: config_item("path", [ "" ]), // User-defined rules to include inside queue chain.
+};
+
+
+let _snort_config_doc =
+"
+This is not an exhaustive list of configuration items, just those that
+require more explanation than is given in the tables that define them, below.
+
+https://openwrt.org/docs/guide-user/services/snort
+
+snort
+ manual - When set to 1, use manual configuration for legacy behavior.
+ When disabled, then use this config.
+ interface - Default should usually be 'uci get network.wan.device',
+ something like 'eth0'
+ home_net - IP range/ranges to protect. May be 'any', but more likely it's
+ your lan range, default is '192.168.1.0/24'
+ external_net - IP range external to home. Usually 'any', but if you only
+ care about true external hosts (trusting all lan devices),
+ then '!$HOMENET' or some specific range
+ mode - 'ids' or 'ips', for detection-only or prevention, respectively
+ oinkcode - https://www.snort.org/oinkcodes
+ config_dir - Location of the base snort configuration files. Default /etc/snort
+ temp_dir - Location of all transient snort config, including downloaded rules
+ Default /var/snort.d
+ logging - Enable external logging of events thus enabling 'snort-mgr report',
+ otherwise events only go to system log (i.e., 'logread -e snort:')
+ log_dir - Location of the generated logs, and oh-by-the-way the snort
+ PID file (why?). Default /var/log
+ openappid - Enabled inspection using the 'openappid' package
+ See 'opkg info openappid'
+ action - 'alert', 'block', 'reject' or 'drop'
+ method - 'pcap', 'afpacket' or 'nfq'
+ snaplen - int daq.snaplen = 1518: set snap length (same as -s) { 0:65535 }
+
+nfq - https://github.com/snort3/libdaq/blob/master/modules/nfq/README.nfq.md
+ queue_maxlen - nfq's '--daq-var queue_maxlen=int'
+ queue_count - Count of queues to use when method=nfq, usually 2-8
+ fanout_type - Sets kernel load balancing algorithm*, one of hash, lb, cpu,
+ rollover, rnd, qm.
+ thread_count - int snort.-z: <count> maximum number of packet threads
+ (same as --max-packet-threads); 0 gets the number of
+ CPU cores reported by the system; default is 1 { 0:max32 }
+ chain_type - Chain type when generating nft output
+ chain_priority - Chain priority when generating nft output
+ include - Full path to user-defined extra rules to include inside queue chain
+
+ * - for details on fanout_type, see these pages:
+ https://github.com/florincoras/daq/blob/master/README
+ https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt
+";
+
+function snort_config_doc(comment) {
+ if (comment == null) comment = "";
+ if (comment != "") comment += " ";
+ for (let line in split(_snort_config_doc, "\n")) {
+ let msg = rtrim(sprintf("%s%s", comment, line));
+ print(msg, "\n");
+ }
+}
+
+//------------------------------------------------------------------------------
+
+function load(section, config) {
+ let self = {
+ ".name": section,
+ ".config": config,
+ };
+
+ // Set the defaults from definitions in table.
+ for (let item in config) {
+ self[item] = config[item].default;
+ }
+
+ // Overwrite them with any uci config settings.
+ let cfg = uci.get_all("snort", section);
+ for (let item in cfg) {
+ // If you need to rename, delete or change the meaning of a
+ // config item, just intercept it and do the work here.
+
+ if (exists(config, item)) {
+ let val = cfg[item];
+ if (config[item].contains(val))
+ self[item] = val;
+ else {
+ wrn(`In option ${item}='${val}', must be ${config[item].allowed()}`);
+ // ??? self[item] = config[item][0]; ???
+ }
+ }
+ }
+
+ return self;
+}
+
+let snort = null;
+let nfq = null;
+function load_all() {
+ snort = load("snort", snort_config);
+ nfq = load("nfq", nfq_config);
+}
+
+function dump_config(settings) {
+ let section = settings[".name"];
+ let config = settings[".config"];
+ printf("config %s '%s'\n", section, section);
+ for (let item in config) {
+ printf("\toption %-15s %-17s# %s\n", item, `'${settings[item]}'`, config[item].allowed());
+ }
+ print("\n");
+}
+
+function render_snort() {
+ include("templates/snort.uc", { snort, nfq });
+}
+
+function render_nftables() {
+ include("templates/nftables.uc", { snort, nfq });
+}
+
+function render_config() {
+ snort_config_doc("#");
+ dump_config(snort);
+ dump_config(nfq);
+}
+
+function render_help() {
+ snort_config_doc();
+}
+
+//------------------------------------------------------------------------------
+
+load_all();
+
+switch (getenv("TYPE")) {
+ case "snort":
+ render_snort();
+ return;
+
+ case "nftables":
+ render_nftables();
+ return;
+
+ case "config":
+ render_config();
+ return;
+
+ case "help":
+ render_help();
+ return;
+
+ default:
+ print("Invalid table type.\n");
+ return;
+}
+
+//------------------------------------------------------------------------------
+-%}
diff --git a/net/snort3/files/nftables.uc b/net/snort3/files/nftables.uc
new file mode 100644
index 000000000..c87246b44
--- /dev/null
+++ b/net/snort3/files/nftables.uc
@@ -0,0 +1,18 @@
+# Do not edit, automatically generated. See /usr/share/snort/templates.
+{%
+// Copyright (c) 2023 Eric Fahlgren <eric.fahlgren@gmail.com>
+// SPDX-License-Identifier: GPL-2.0
+
+let queues = `${nfq.queue_start}-${int(nfq.queue_start)+int(nfq.queue_count)-1}`;
+let chain_type = nfq.chain_type;
+-%}
+
+table inet snort {
+ chain {{ chain_type }}_{{ snort.mode }} {
+ type filter hook {{ chain_type }} priority {{ nfq.chain_priority }}
+ policy accept
+ {% if (nfq.include) { include(nfq.include, { snort, nfq }); } %}
+ # tcp flags ack ct direction original ct state established counter accept
+ counter queue flags bypass to {{ queues }}
+ }
+}
diff --git a/net/snort3/files/snort-mgr b/net/snort3/files/snort-mgr
new file mode 100644
index 000000000..6a5e85e22
--- /dev/null
+++ b/net/snort3/files/snort-mgr
@@ -0,0 +1,260 @@
+#!/bin/sh
+# Copyright (c) 2023 Eric Fahlgren <eric.fahlgren@gmail.com>
+# SPDX-License-Identifier: GPL-2.0
+# shellcheck disable=SC2039 # "local" not defined in POSIX sh
+
+PROG="/usr/bin/snort"
+MAIN="/usr/share/snort/main.uc"
+CONF_DIR="/var/snort.d"
+CONF="${CONF_DIR}/snort_conf.lua"
+
+VERBOSE=
+TESTING=
+NLINES=0
+
+[ ! -e "$CONF_DIR" ] && mkdir "$CONF_DIR"
+[ -e /dev/stdin ] && STDIN=/dev/stdin || STDIN=/proc/self/fd/0
+[ -e /dev/stdout ] && STDOUT=/dev/stdout || STDOUT=/proc/self/fd/1
+[ -t 2 ] && export TTY=1
+
+die() {
+ [ -n "$QUIET" ] || echo "$@" >&2
+ exit 1
+}
+
+disable_offload()
+{
+ # From https://forum.openwrt.org/t/snort-3-nfq-with-ips-mode/161172
+ # https://blog.snort.org/2016/08/running-snort-on-commodity-hardware.html
+ # Not needed when running the nft daq as defragmentation is done by the kernel.
+ # What about pcap?
+
+ local filter_method=$(uci -q get snort.snort.method)
+ if [ "$filter_method" = "afpacket" ]; then
+ local wan=$(uci get snort.snort.interface)
+ if [ -n "$wan" ] && ethtool -k "$wan" | grep -q -E '(tcp-segmentation-offload|receive-offload): on' ; then
+ ethtool -K "$wan" gro off lro off tso off 2> /dev/null
+ log "Disabled gro, lro and tso on '$wan' using ethtool."
+ fi
+ fi
+}
+
+nft_rm_table() {
+ for table_type in 'inet' 'netdev'; do
+ nft list tables | grep -q "${table_type} snort" && nft delete table "${table_type}" snort
+ done
+}
+
+nft_add_table() {
+ if [ "$(uci -q get snort.snort.method)" = "nfq" ]; then
+ print nftables | nft $VERBOSE -f $STDIN
+ [ -n "$VERBOSE" ] && nft list table inet snort
+ fi
+}
+
+setup() {
+ # Generates all the configuration, then reports the config file for snort.
+ # Does NOT generate the rules file, you'll need to do 'update-rules' first.
+ nft_rm_table
+ print snort > "$CONF"
+ nft_add_table
+ echo "$CONF"
+}
+
+teardown() {
+ # Merely cleans up after.
+ nft_rm_table
+ [ -e "$CONF" ] && rm "$CONF"
+}
+
+update_rules() {
+ /usr/bin/snort-rules $TESTING
+}
+
+print() {
+ # '$1' is file type to generate, one of:
+ # config, snort or nftables
+ TYPE=$1 utpl -S "$MAIN"
+}
+
+check() {
+ local manual=$(uci get snort.snort.manual)
+ [ "$manual" = 1 ] && return 0
+
+ [ -n "$QUIET" ] && OUT=/dev/null || OUT=$STDOUT
+ local test_conf="${CONF_DIR}/test_conf.lua"
+ print snort > "${test_conf}" || die "Errors during generation of config."
+ if $PROG -T -q --warn-all -c "${test_conf}" 2> $OUT ; then
+ rm "${test_conf}"
+ return 0
+ fi
+ die "Errors in snort config tests."
+}
+
+report() {
+ # Reported IPs have source port stripped, but destination port (if any)
+ # retained.
+ #
+ # json notes
+ # from alert_fast:
+ # 08/30-11:39:57.639021 [**] [1:382:11] "PROTOCOL-ICMP PING Windows" [**] [Classification: Misc activity] [Priority: 3] {ICMP} 10.1.1.186 -> 10.1.1.20
+ #
+ # same event in alert_json (single line broken for clarity):
+ # { "timestamp" : "08/30-11:39:57.639021", "pkt_num" : 5366, "proto" : "ICMP", "pkt_gen" : "raw",
+ # "pkt_len" : 60, "dir" : "C2S", "src_ap" : "10.1.1.186:0", "dst_ap" : "10.1.1.20:0",
+ # "rule" : "1:382:11", "action" : "allow" }
+ #
+ # Second part of "rule", 382, is "sid" in ruleset, suffixing 11 is "rev".
+ # grep '\bsid:382\b' /etc/snort/rules/snort.rules (again, single line broken for clarity):
+ # alert icmp $EXTERNAL_NET any -> $HOME_NET any ( msg:"PROTOCOL-ICMP PING Windows";
+ # itype:8; content:"abcdefghijklmnop",depth 16; metadata:ruleset community;
+ # classtype:misc-activity; sid:382; rev:11; )
+ #
+ # Not sure where the prefixing 1 comes from.
+
+ local logging=$(uci get snort.snort.logging)
+ local log_dir=$(uci get snort.snort.log_dir)
+ local pattern="$1"
+
+ if [ "$logging" = 0 ]; then
+ die "Logging is not enabled in snort config."
+ fi
+
+ #if [ -z "$pattern" ]; then
+ # die "Provide a valid IP and try again."
+ #fi
+
+ [ "$NLINES" = 0 ] && output="cat" || output="head -n $NLINES"
+
+ # Fix this to use json file.
+ tmp="/tmp/snort.report.$$"
+ echo "Intrusions involving ${pattern:-all IPs}"
+ grep "\b${pattern}\b" "$log_dir/alert_fast.txt" \
+ | sed 's/.*"\([^"]*\)".* \([^ :]*\)[: ].*-> \(.*\)/\1#\2#\3/' > "$tmp"
+ n_incidents="$(wc -l < $tmp)"
+ lines=$(sort "$tmp" | uniq -c | sort -nr \
+ | awk -F'#' '{printf "%-80s %-12s -> %s\n", $1, $2, $3}')
+ echo "$lines" | $output
+ n_lines=$(echo "$lines" | wc -l)
+ [ "$NLINES" -gt 0 ] && [ "$NLINES" -lt "$n_lines" ] && echo " ... Only showing $NLINES of $n_lines most frequent incidents."
+ printf "%7d total incidents\n" "$n_incidents"
+ rm "$tmp"
+}
+
+status() {
+ echo 'tbd'
+}
+
+
+while [ -n "$1" ]; do
+ case "$1" in
+ -q)
+ export QUIET=1
+ shift
+ ;;
+ -v)
+ export VERBOSE=-e
+ shift
+ ;;
+ -t)
+ TESTING=-t
+ shift
+ ;;
+ -n)
+ NLINES="$2"
+ shift
+ shift
+ ;;
+ *)
+ break
+ ;;
+ esac
+done
+
+case "$1" in
+ setup)
+ setup
+ ;;
+ teardown)
+ teardown
+ ;;
+ resetup)
+ QUIET=1 check || die "The generated snort lua configuration contains errors, not restarting."
+ teardown
+ setup
+ ;;
+ update-rules)
+ update_rules
+ ;;
+ check)
+ check
+ ;;
+ print)
+ print "$2"
+ ;;
+ report)
+ report "$2"
+ ;;
+ status)
+ status
+ ;;
+ *)
+ cat <<USAGE
+Usage:
+
+ -n = show only NLINES of output
+ -q = quiet
+ -v = verbose
+ -t = testing mode
+
+ $0 [-v] [-q] setup|teardown|resetup
+
+ Normally only used internally by init scripts to manage the generation
+ of configuration files and any needed firewall rules. None of these
+ modify the snort rules in any way (see 'update-rules').
+ setup = generates snort config, sets up firewall.
+ teardown = removes any firewall rules.
+ resetup = shorthand for teardown and then setup.
+
+
+ $0 [-n lines] report [pattern]
+
+ Report on incidents. Note this is somewhat experimental, so suggested
+ improvements are quite welcome.
+ pattern = IP or piece of IP or something in the message to filter.
+
+ $0 [-t] update-rules
+
+ Download and install the snort ruleset. Testing mode generates a canned
+ rule that matches IPv4 ping requests. A typical test scenario might look
+ like:
+
+ > snort-mgr -t update-rules
+ > /etc/init.d/snort start
+ > ping -c4 8.8.8.8
+ > logread -e "TEST ALERT"
+
+
+ $0 print config|snort|nftables
+
+ Print the rendered file contents.
+ config = Display contents of /etc/config/snort, but with all values and
+ descriptions. Missing values shown with defaults.
+ snort = The snort configuration file, which is a lua script.
+ nftables = The nftables script used to define the input queues when using
+ the 'nfq' DAQ.
+
+
+ $0 [-q] check
+
+ Test the rendered config using snort's check mode without
+ applying it to the running system.
+
+
+ $0 status
+
+ Print the nfq counter values and blah blah blah
+
+USAGE
+ ;;
+esac
diff --git a/net/snort3/files/snort-rules b/net/snort3/files/snort-rules
new file mode 100644
index 000000000..24ae7a7f7
--- /dev/null
+++ b/net/snort3/files/snort-rules
@@ -0,0 +1,92 @@
+#!/bin/sh
+# Copyright (c) 2023 Eric Fahlgren <eric.fahlgren@gmail.com>
+# SPDX-License-Identifier: GPL-2.0
+# shellcheck disable=SC2039 # "local" not defined in POSIX sh
+
+alias log='logger -s -t "snort-rules[$$]" -p "info"'
+
+[ "$1" = "-t" ] && testing=true || testing=false
+
+download_rules() {
+ # Further information:
+ # https://www.snort.org/products#rule_subscriptions
+ # https://www.snort.org/oinkcodes
+ #
+ # Also, what to do about "subscription" vs Talos_LightSPD rules when subbed?
+ # Add a "use_rules" list or option or something?
+ oinkcode=$(uci -q get snort.snort.oinkcode)
+
+
+
+ local conf_dir=$(uci -q get snort.snort.config_dir || echo "/etc/snort")
+ local rules_file="$conf_dir/rules/snort.rules"
+ local data_dir=$(uci -q get snort.snort.temp_dir || echo "/var/snort.d")
+ local data_tar="$data_dir/rules.tar.gz"
+
+ # Make sure everything exists.
+ [ -d "$data_dir" ] || mkdir -p "$data_dir"
+
+
+ if $testing ; then
+ log "Generating testing rules..."
+ new_rules="$data_dir/testing.rules"
+ rm -f "$new_rules"
+ echo 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v4"; icode:0; itype: 8; sid:10000010; rev:001;)' >> "$new_rules"
+ #echo 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:33; sid:10000011; rev:001;)' >> "$new_rules"
+ #echo 'alert icmp any any <> any any (msg:"TEST ALERT ICMP v6"; icode:0; itype:34; sid:10000012; rev:001;)' >> "$new_rules"
+
+ else
+ if [ -z "$oinkcode" ]; then
+ # If you do not have a subscription, then we use the community rules:
+ log "Downloading community rules..."
+ url="https://www.snort.org/downloads/community/snort3-community-rules.tar.gz"
+
+ else
+ # If you have a subscription and its corresponding oinkcode, use this:
+ #
+ # 'snortver' is the version number of the snort executable in use on your
+ # router.
+ #
+ # Ideally, the 'snort --version' output would work, but OpenWrt builds
+ # are often between (or, more likely, newer than) those listed on the
+ # snort.org downloads page.
+ #
+ # So instead, we define it manually to be the value just before the
+ # installed version. Look on https://www.snort.org/advisories/ and
+ # select the most recent date. On that page, find the closest version
+ # number preceding your installed version and modify the hard-coded
+ # value below (for example, installed is 31600 then use 31470):
+
+ #snortver=$(snort --version | awk '/Version/ {print gensub("\\.", "", "", $NF)}')
+ snortver=31470
+
+ log "Downloading subscription rules..."
+ url="https://www.snort.org/rules/snortrules-snapshot-$snortver.tar.gz?oinkcode=$oinkcode"
+ fi
+
+ wget "$url" -O "$data_tar" 2>&1 | log || exit 1
+
+ # ??? Does non-community tar contain just the one "*.rules" file, too???
+ new_rules=$(tar tzf "$data_tar" | grep '\.rules$')
+ new_rules="$data_dir/$new_rules"
+
+ old_rules="$data_dir/old.rules"
+ if [ -e "$new_rules" ]; then
+ # Before we overwrite with the new download.
+ log "Stashing old rules to $old_rules ..."
+ mv -f "$new_rules" "$old_rules"
+ fi
+
+ log "Unpacking $data_tar ..."
+ tar xzvf "$data_tar" -C "$data_dir" | log || exit 1
+ if [ -e "$old_rules" ] && ! cmp -s "$new_rules" "$old_rules" ; then
+ diff "$new_rules" "$old_rules" 2>&1 | log
+ fi
+ fi
+
+ rm -f "$rules_file"
+ ln -s "$new_rules" "$rules_file"
+
+ log "Snort rules loaded, restart snort now."
+}
+download_rules
diff --git a/net/snort3/files/snort.config b/net/snort3/files/snort.config
index 84f5e96d9..5567ef464 100644
--- a/net/snort3/files/snort.config
+++ b/net/snort3/files/snort.config
@@ -1,3 +1,74 @@
+#
+# This is not an exhaustive list of configuration items, just those that
+# require more explanation than is given in the tables that define them, below.
+#
+# https://openwrt.org/docs/guide-user/services/snort
+#
+# snort
+# manual - When set to 1, use manual configuration for legacy behavior.
+# When disabled, then use this config.
+# interface - Default should usually be 'uci get network.wan.device',
+# something like 'eth0'
+# home_net - IP range/ranges to protect. May be 'any', but more likely it's
+# your lan range, default is '192.168.1.0/24'
+# external_net - IP range external to home. Usually 'any', but if you only
+# care about true external hosts (trusting all lan devices),
+# then '!$HOMENET' or some specific range
+# mode - 'ids' or 'ips', for detection-only or prevention, respectively
+# oinkcode - https://www.snort.org/oinkcodes
+# config_dir - Location of the base snort configuration files. Default /etc/snort
+# temp_dir - Location of all transient snort config, including downloaded rules
+# Default /var/snort.d
+# logging - Enable external logging of events thus enabling 'snort-mgr report',
+# otherwise events only go to system log (i.e., 'logread -e snort:')
+# log_dir - Location of the generated logs, and oh-by-the-way the snort
+# PID file (why?). Default /var/log
+# openappid - Enabled inspection using the 'openappid' package
+# See 'opkg info openappid'
+# action - 'alert', 'block', 'reject' or 'drop'
+# method - 'pcap', 'afpacket' or 'nfq'
+# snaplen - int daq.snaplen = 1518: set snap length (same as -s) { 0:65535 }
+#
+# nfq - https://github.com/snort3/libdaq/blob/master/modules/nfq/README.nfq.md
+# queue_maxlen - nfq's '--daq-var queue_maxlen=int'
+# queue_count - Count of queues to use when method=nfq, usually 2-8
+# fanout_type - Sets kernel load balancing algorithm*, one of hash, lb, cpu,
+# rollover, rnd, qm.
+# thread_count - int snort.-z: <count> maximum number of packet threads
+# (same as --max-packet-threads); 0 gets the number of
+# CPU cores reported by the system; default is 1 { 0:max32 }
+# chain_type - Chain type when generating nft output
+# chain_priority - Chain priority when generating nft output
+# include - Full path to user-defined extra rules to include inside queue chain
+#
+# * - for details on fanout_type, see these pages:
+# https://github.com/florincoras/daq/blob/master/README
+# https://www.kernel.org/doc/Documentation/networking/packet_mmap.txt
+#
config snort 'snort'
- option config_dir '/etc/snort/'
- option interface 'eth0'
+ option enabled '0' # one of [0, 1]
+ option manual '1' # one of [0, 1]
+ option oinkcode '' # a string
+ option home_net '192.168.1.0/24' # a string
+ option external_net 'any' # a string
+ option config_dir '/etc/snort' # a path string
+ option temp_dir '/var/snort.d' # a path string
+ option log_dir '/var/log' # a path string
+ option logging '1' # one of [0, 1]
+ option openappid '0' # one of [0, 1]
+ option mode 'ids' # one of [ids, ips]
+ option method 'pcap' # one of [pcap, afpacket, nfq]
+ option action 'alert' # one of [alert, block, drop, reject]
+ option interface 'eth0' # a string
+ option snaplen '1518' # 1518 <= x <= 65535
+
+config nfq 'nfq'
+ option queue_count '4' # 1 <= x <= 16
+ option queue_start '4' # 1 <= x <= 32768
+ option queue_maxlen '1024' # 1024 <= x <= 65536
+ option fanout_type 'hash' # one of [hash, lb, cpu, rollover, rnd, qm]
+ option thread_count '0' # 0 <= x <= 32
+ option chain_type 'input' # one of [prerouting, input, forward, output, postrouting]
+ option chain_priority 'filter' # one of [raw, filter, 300]
+ option include '' # a path string
+
diff --git a/net/snort3/files/snort.init b/net/snort3/files/snort.init
index ff864e02b..f73ebe879 100644
--- a/net/snort3/files/snort.init
+++ b/net/snort3/files/snort.init
@@ -1,36 +1,58 @@
#!/bin/sh /etc/rc.common
+# shellcheck disable=SC2039 # "local" not defined in POSIX sh
START=99
STOP=10
USE_PROCD=1
PROG=/usr/bin/snort
+MGR=/usr/bin/snort-mgr
validate_snort_section() {
+ $MGR -q check || return 1
uci_validate_section snort snort "${1}" \
+ 'enabled:bool:0' \
+ 'manual:bool:1' \
'config_dir:string' \
'interface:string'
}
start_service() {
- local config_file interface
+ # If you wish to use application-managed PID file:
+ # output.logdir, in the snort lua config, determines the PID file location.
+ # Add '--create-pidfile' to the 'command', below.
- validate_snort_section snort || {
- echo "validation failed"
- return 1
- }
+ local enabled
+ local manual
+ local config_dir
+ local interface
+
+ validate_snort_section snort || {
+ echo "Validation failed, try 'snort-mgr check'."
+ return 1
+ }
+
+ [ "$enabled" = 0 ] && return
procd_open_instance
- procd_set_param command $PROG -q -i "$interface" -c "${config_dir%/}/snort.lua" --tweaks local
- procd_set_param env SNORT_LUA_PATH="$config_dir"
- procd_set_param file $CONFIGFILE
+ if [ "$manual" = 0 ]; then
+ local config_file=$($MGR setup)
+ procd_set_param command "$PROG" -q -c "${config_file}"
+ else
+ procd_set_param command $PROG -q -i "$interface" -c "${config_dir%/}/snort.lua" --tweaks local
+ procd_set_param env SNORT_LUA_PATH="$config_dir"
+ procd_set_param file $CONFIGFILE
+ fi
procd_set_param respawn
+ procd_set_param stdout 0
+ procd_set_param stderr 1
procd_close_instance
}
stop_service()
{
- service_stop ${PROG}
+ service_stop "$PROG"
+ $MGR teardown
}
service_triggers()
diff --git a/net/snort3/files/snort.uc b/net/snort3/files/snort.uc
new file mode 100644
index 000000000..b58fa01e6
--- /dev/null
+++ b/net/snort3/files/snort.uc
@@ -0,0 +1,126 @@
+{%
+// Copyright (c) 2023 Eric Fahlgren <eric.fahlgren@gmail.com>
+// SPDX-License-Identifier: GPL-2.0
+
+// Create some snort-format-specific items.
+
+let home_net = snort.home_net == 'any' ? "'any'" : snort.home_net;
+let external_net = snort.external_net;
+
+let line_mode = snort.mode == "ids" ? "tap" : "inline";
+
+let inputs = null;
+let vars = null;
+switch (snort.method) {
+case "pcap":
+case "afpacket":
+ inputs = `{ '${snort.interface}' }`;
+ vars = "{}";
+ break;
+
+case "nfq":
+ inputs = "{ ";
+ for (let i = int(nfq.queue_start); i < int(nfq.queue_start)+int(nfq.queue_count); i++) {
+ inputs += `'${i}', `
+ }
+ inputs += "}";
+
+ vars = `{ 'device=${snort.interface}', 'queue_maxlen=${nfq.queue_maxlen}', 'fanout_type=${nfq.fanout_type}', 'fail_open', }`;
+ break;
+}
+-%}
+-- Do not edit, automatically generated. See /usr/share/snort/templates.
+
+-- These must be defined before processing snort.lua
+-- The default include '/etc/snort/homenet.lua' must not redefine them.
+HOME_NET = [[ {{ home_net }} ]]
+EXTERNAL_NET = '{{ external_net }}'
+
+include('{{ snort.config_dir }}/snort.lua')
+
+snort = {
+{% if (snort.mode == 'ips'): %}
+ ['-Q'] = true,
+{% endif %}
+ ['--daq'] = {{ snort.method }},
+--['--daq-dir'] = '/usr/lib/daq/',
+{% if (snort.method == 'nfq'): %}
+ ['--max-packet-threads'] = {{ nfq.thread_count }},
+{% endif %}
+}
+
+ips = {
+ mode = {{ line_mode }},
+ variables = default_variables,
+ action_override = {{ snort.action }},
+ include = "{{ snort.config_dir }}/" .. RULE_PATH .. '/snort.rules',
+}
+
+daq = {
+ inputs = {{ inputs }},
+ snaplen = {{ snort.snaplen }},
+ module_dirs = { '/usr/lib/daq/', },
+ modules = {
+ {
+ name = '{{ snort.method }}',
+ mode = {{ line_mode }},
+ variables = {{ vars }},
+ }
+ }
+}
+
+alert_syslog = {
+ level = 'info',
+}
+
+{% if (int(snort.logging)): %}
+-- Note that this is also the location of the PID file, if you use it.
+output.logdir = "{{ snort.log_dir }}"
+
+-- Maybe add snort.log_type, 'fast', 'json' and 'full'?
+-- Json would be best for reporting, see 'snort-mgr report' code.
+-- alert_full = { file = true, }
+
+alert_fast = {
+-- bool alert_fast.file = false: output to alert_fast.txt instead of stdout
+-- bool alert_fast.packet = false: output packet dump with alert
+-- int alert_fast.limit = 0: set maximum size in MB before rollover (0 is unlimited) { 0:maxSZ }
+ file = true,
+ packet = false,
+}
+alert_json = {
+-- bool alert_json.file = false: output to alert_json.txt instead of stdout
+-- multi alert_json.fields = timestamp pkt_num proto pkt_gen pkt_len dir src_ap dst_ap rule action: selected fields will be output
+-- int alert_json.limit = 0: set maximum size in MB before rollover (0 is unlimited) { 0:maxSZ }
+-- string alert_json.separator = , : separate fields with this character sequence
+ file = true,
+}
+
+{% endif -%}
+
+normalizer = {
+ tcp = {
+ ips = true,
+ }
+}
+
+file_policy = {
+ enable_type = true,
+ enable_signature = true,
+ rules = {
+ use = {
+ verdict = 'log',
+ enable_file_type = true,
+ enable_file_signature = true,
+ }
+ }
+}
+
+-- To use openappid with snort, 'opkg install openappid' and enable in config.
+{% if (int(snort.openappid)): %}
+appid = {
+ log_stats = true,
+ app_detector_dir = '/usr/lib/openappid',
+ app_stats_period = 60,
+}
+{% endif %}