From f21dffc2a306ad97cefa03dac8bcee0552da556f Mon Sep 17 00:00:00 2001 From: Eric Fahlgren Date: Mon, 27 Nov 2023 08:21:43 -0800 Subject: [PATCH] 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 --- net/snort3/Makefile | 21 ++- net/snort3/files/homenet.lua | 5 +- net/snort3/files/local.lua | 3 + net/snort3/files/main.uc | 263 ++++++++++++++++++++++++++++++++++ net/snort3/files/nftables.uc | 18 +++ net/snort3/files/snort-mgr | 260 +++++++++++++++++++++++++++++++++ net/snort3/files/snort-rules | 92 ++++++++++++ net/snort3/files/snort.config | 75 +++++++++- net/snort3/files/snort.init | 40 ++++-- net/snort3/files/snort.uc | 126 ++++++++++++++++ 10 files changed, 888 insertions(+), 15 deletions(-) create mode 100644 net/snort3/files/main.uc create mode 100644 net/snort3/files/nftables.uc create mode 100644 net/snort3/files/snort-mgr create mode 100644 net/snort3/files/snort-rules create mode 100644 net/snort3/files/snort.uc diff --git a/net/snort3/Makefile b/net/snort3/Makefile index 5f6b50cc8f..3f4df09967 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 975f702541..91845611d3 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 c48ffd0c8b..8de694131d 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 0000000000..7db420f339 --- /dev/null +++ b/net/snort3/files/main.uc @@ -0,0 +1,263 @@ +{% +//------------------------------------------------------------------------------ +// Copyright (c) 2023 Eric Fahlgren +// 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: 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 0000000000..c87246b441 --- /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 +// 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 0000000000..6a5e85e228 --- /dev/null +++ b/net/snort3/files/snort-mgr @@ -0,0 +1,260 @@ +#!/bin/sh +# Copyright (c) 2023 Eric Fahlgren +# 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 < 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 0000000000..24ae7a7f7b --- /dev/null +++ b/net/snort3/files/snort-rules @@ -0,0 +1,92 @@ +#!/bin/sh +# Copyright (c) 2023 Eric Fahlgren +# 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 84f5e96d91..5567ef4646 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: 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 ff864e02b2..f73ebe8799 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 0000000000..b58fa01e6d --- /dev/null +++ b/net/snort3/files/snort.uc @@ -0,0 +1,126 @@ +{% +// Copyright (c) 2023 Eric Fahlgren +// 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 %}