snort3: complete rework
authorEric Fahlgren <ericfahlgren@gmail.com>
Mon, 27 Nov 2023 16:21:43 +0000 (08:21 -0800)
committerRosen Penev <rosenp@gmail.com>
Sun, 3 Dec 2023 21:53:58 +0000 (13:53 -0800)
  - 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>
net/snort3/Makefile
net/snort3/files/homenet.lua
net/snort3/files/local.lua
net/snort3/files/main.uc [new file with mode: 0644]
net/snort3/files/nftables.uc [new file with mode: 0644]
net/snort3/files/snort-mgr [new file with mode: 0644]
net/snort3/files/snort-rules [new file with mode: 0644]
net/snort3/files/snort.config
net/snort3/files/snort.init
net/snort3/files/snort.uc [new file with mode: 0644]

index 5f6b50cc8fc20361cc2ac25302e6173399d374ea..3f4df0996783e0b686e67a09bd5bfcaec63af5a9 100644 (file)
@@ -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) \
index 975f7025418d5f9da75f8e7ccac9d1efa8ace233..91845611d3c5a4a8207a1e1f95a97472ad16033d 100644 (file)
@@ -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"
index c48ffd0c8bc637181c9252b21d7af901c91ed9db..8de694131d86378806579d561237ee19f3cfc002 100644 (file)
@@ -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 (file)
index 0000000..7db420f
--- /dev/null
@@ -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 (file)
index 0000000..c87246b
--- /dev/null
@@ -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 (file)
index 0000000..6a5e85e
--- /dev/null
@@ -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 (file)
index 0000000..24ae7a7
--- /dev/null
@@ -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
index 84f5e96d91d5fe1b4510022265439ef0c70f0b45..5567ef46468d9e2a4bacf6ef5832e0a527af8262 100644 (file)
@@ -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
+
index ff864e02b273abfe1ad2c87b56863ffe412bbc1e..f73ebe8799c88aab7f3793bb1a534e2928726053 100644 (file)
@@ -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 (file)
index 0000000..b58fa01
--- /dev/null
@@ -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 %}