poc4 wit rust

This commit is contained in:
Mateusz Gruszczyński
2026-08-16 15:34:53 +02:00
parent e5d344622e
commit 40474cdc59
29 changed files with 2692 additions and 159 deletions
+165
View File
@@ -0,0 +1,165 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
CONFIG_FILE="${DEPLOY_CONFIG:-deploy-routeros.env}"
if [ -f "$CONFIG_FILE" ]; then
# shellcheck disable=SC1090
case "$CONFIG_FILE" in
/*) . "$CONFIG_FILE" ;;
*) . "./$CONFIG_FILE" ;;
esac
fi
: "${ROUTER_HOST:=192.168.88.1}"
: "${ROUTER_USER:=admin}"
: "${ROUTER_PORT:=22}"
: "${ROUTER_IDENTITY_FILE:=}"
: "${ROUTER_SCP_DIR:=/}"
: "${CONTAINER_IP:=172.31.255.2/30}"
: "${TZSP_PORT:=37008}"
: "${CONFIGURE_IPV4_MANGLE:=true}"
: "${CONFIGURE_L2_SNIFFER:=true}"
: "${START_L2_SNIFFER:=true}"
: "${TZSP_L2_INTERFACE:=}"
: "${TZSP_L2_MAC_PROTOCOL:=!ip}"
TZSP_L2_FILTER_INTERFACE="${TZSP_L2_INTERFACE:-all}"
: "${KEEP_REMOTE_RSC:=false}"
: "${DRY_RUN:=false}"
case "$ROUTER_PORT" in *[!0-9]*|'') echo "ROUTER_PORT must be numeric" >&2; exit 2 ;; esac
case "$TZSP_PORT" in *[!0-9]*|'') echo "TZSP_PORT must be numeric" >&2; exit 2 ;; esac
for bool_pair in \
"CONFIGURE_IPV4_MANGLE=$CONFIGURE_IPV4_MANGLE" \
"CONFIGURE_L2_SNIFFER=$CONFIGURE_L2_SNIFFER" \
"START_L2_SNIFFER=$START_L2_SNIFFER" \
"DRY_RUN=$DRY_RUN"
do
case "${bool_pair#*=}" in
true|false) ;;
*) echo "${bool_pair%%=*} must be true or false" >&2; exit 2 ;;
esac
done
check_ros_value() {
label="$1"
value="$2"
case "$value" in
*'"'*|*'\\'*|*'$'*|*';'*|*'`'*)
echo "$label contains a character not supported by this helper: $value" >&2
exit 3
;;
esac
}
check_ros_value CONTAINER_IP "$CONTAINER_IP"
check_ros_value TZSP_L2_INTERFACE "$TZSP_L2_INTERFACE"
check_ros_value TZSP_L2_MAC_PROTOCOL "$TZSP_L2_MAC_PROTOCOL"
TARGET="${ROUTER_USER}@${ROUTER_HOST}"
ssh_run() {
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
ssh -i "$ROUTER_IDENTITY_FILE" -p "$ROUTER_PORT" "$TARGET" "$1"
else
ssh -p "$ROUTER_PORT" "$TARGET" "$1"
fi
}
scp_put() {
src="$1"
dst="$2"
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
scp -i "$ROUTER_IDENTITY_FILE" -P "$ROUTER_PORT" "$src" "${TARGET}:$dst"
else
scp -P "$ROUTER_PORT" "$src" "${TARGET}:$dst"
fi
}
CONTAINER_IP_ONLY="${CONTAINER_IP%/*}"
ID="$(date -u +%Y%m%d%H%M%S)"
mkdir -p build
LOCAL_RSC="build/configure-tzsp-hybrid-${ID}.rsc"
REMOTE_NAME="configure-tzsp-hybrid-${ID}.rsc"
REMOTE_PATH="${ROUTER_SCP_DIR%/}/${REMOTE_NAME}"
[ "$ROUTER_SCP_DIR" = "/" ] && REMOTE_PATH="/${REMOTE_NAME}"
cat > "$LOCAL_RSC" <<RSC
# MikroSuricata hybrid TZSP capture.
# Generated by scripts/configure-routeros-tzsp-hybrid.sh at ${ID} UTC.
RSC
if [ "$CONFIGURE_IPV4_MANGLE" = "true" ]; then
cat >> "$LOCAL_RSC" <<RSC
# Routed IPv4: high-throughput path. Only the MikroSuricata-owned rule is replaced.
/ip/firewall/mangle/remove [find where comment="MikroSuricata TZSP IPv4"]
:if ([:len [/ip/firewall/mangle/find]] > 0) do={
/ip/firewall/mangle/add chain=forward action=sniff-tzsp sniff-target="${CONTAINER_IP_ONLY}" sniff-target-port=${TZSP_PORT} comment="MikroSuricata TZSP IPv4" place-before=0
} else={
/ip/firewall/mangle/add chain=forward action=sniff-tzsp sniff-target="${CONTAINER_IP_ONLY}" sniff-target-port=${TZSP_PORT} comment="MikroSuricata TZSP IPv4"
}
RSC
fi
if [ "$CONFIGURE_L2_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<RSC
# Non-IPv4 Ethernet: low-volume Packet Sniffer complement.
# Empty TZSP_L2_INTERFACE intentionally means all interfaces; no bridge name is assumed.
/tool/sniffer/stop
:if ("${TZSP_L2_INTERFACE}" != "") do={
:if ([:len [/interface/find where name="${TZSP_L2_INTERFACE}"]] = 0) do={
:error "TZSP_L2_INTERFACE not found: ${TZSP_L2_INTERFACE}"
}
}
/tool/sniffer/set only-headers=no max-packet-size=2048 streaming-enabled=yes filter-stream=yes filter-interface="${TZSP_L2_FILTER_INTERFACE}" filter-mac-address="" filter-src-mac-address="" filter-dst-mac-address="" filter-mac-protocol=${TZSP_L2_MAC_PROTOCOL} filter-ip-address="" filter-src-ip-address="" filter-dst-ip-address="" filter-ipv6-address="" filter-src-ipv6-address="" filter-dst-ipv6-address="" filter-ip-protocol="" filter-port="" filter-src-port="" filter-dst-port="" filter-vlan="" filter-cpu="" filter-size="" filter-direction=any filter-operator-between-entries=or
:do {
/tool/sniffer/set streaming-server=${CONTAINER_IP_ONLY}:${TZSP_PORT}
} on-error={
/tool/sniffer/set streaming-server=${CONTAINER_IP_ONLY} streaming-port=${TZSP_PORT}
}
RSC
if [ "$START_L2_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<'RSC'
/tool/sniffer/start
RSC
fi
fi
cat >> "$LOCAL_RSC" <<'RSC'
/ip/firewall/mangle/print stats where comment="MikroSuricata TZSP IPv4"
/tool/sniffer/print
RSC
if [ "$DRY_RUN" = "true" ]; then
cat "$LOCAL_RSC"
echo "[capture] dry run only; RouterOS was not modified"
exit 0
fi
command -v ssh >/dev/null 2>&1 || { echo "ssh is required" >&2; exit 2; }
command -v scp >/dev/null 2>&1 || { echo "scp is required" >&2; exit 2; }
echo "[capture] preflight ${TARGET}"
ssh_run '/ip/firewall/mangle/print' >/dev/null
if [ "$CONFIGURE_L2_SNIFFER" = "true" ]; then
ssh_run '/tool/sniffer/print' >/dev/null
fi
echo "[capture] uploading $REMOTE_PATH"
scp_put "$LOCAL_RSC" "$REMOTE_PATH"
echo "[capture] applying hybrid capture"
ssh_run "/import file-name=\"${REMOTE_NAME}\""
if [ "$KEEP_REMOTE_RSC" != "true" ]; then
ssh_run "/file/remove [find where name=\"${REMOTE_NAME}\"]" || true
fi
echo "[capture] done"
echo "[capture] IPv4: mangle sniff-tzsp -> ${CONTAINER_IP_ONLY}:${TZSP_PORT}"
if [ "$CONFIGURE_L2_SNIFFER" = "true" ]; then
if [ -n "$TZSP_L2_INTERFACE" ]; then
echo "[capture] non-IPv4: Packet Sniffer on ${TZSP_L2_INTERFACE} -> ${CONTAINER_IP_ONLY}:${TZSP_PORT}"
else
echo "[capture] non-IPv4: Packet Sniffer on all interfaces -> ${CONTAINER_IP_ONLY}:${TZSP_PORT}"
fi
fi
+89 -17
View File
@@ -21,6 +21,14 @@ esac
CONTAINER_NAME="suricata_${VERSION}"
ROOT_DIR="/containers/${CONTAINER_NAME}/root"
# Backward-compatible aliases for pre-0.11.2 deploy-routeros.env files.
if [ "${CONFIGURE_TZSP_CAPTURE+x}" != x ] && [ "${CONFIGURE_SNIFFER+x}" = x ]; then
CONFIGURE_TZSP_CAPTURE="$CONFIGURE_SNIFFER"
fi
if [ "${START_L2_SNIFFER+x}" != x ] && [ "${START_SNIFFER+x}" = x ]; then
START_L2_SNIFFER="$START_SNIFFER"
fi
: "${ROUTER_HOST:=192.168.88.1}"
: "${ROUTER_USER:=admin}"
: "${ROUTER_PORT:=22}"
@@ -32,14 +40,22 @@ ROOT_DIR="/containers/${CONTAINER_NAME}/root"
: "${CONTAINER_SUBNET:=172.31.255.0/30}"
: "${CONTAINER_BRIDGE:=br-ids}"
: "${CONTAINER_VETH:=veth-ids}"
: "${VLAN_ID:=100}"
: "${TZSP_PORT:=37008}"
: "${CONFIGURE_SNIFFER:=true}"
: "${START_SNIFFER:=true}"
: "${CONFIGURE_TZSP_CAPTURE:=true}"
: "${CONFIGURE_IPV4_MANGLE:=true}"
: "${CONFIGURE_L2_SNIFFER:=true}"
: "${START_L2_SNIFFER:=true}"
: "${TZSP_L2_INTERFACE:=}"
: "${TZSP_L2_MAC_PROTOCOL:=!ip}"
TZSP_L2_FILTER_INTERFACE="${TZSP_L2_INTERFACE:-all}"
: "${TZSP_RCVBUF_BYTES:=33554432}"
: "${TZSP_BATCH_SIZE:=256}"
: "${TZSP_QUEUE_MB:=64}"
: "${TZSP_DATAGRAM_BYTES:=12288}"
: "${SURICATA_HOME_NET:=[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]}"
: "${MONITORED_NETWORKS:=192.168.0.0/16,10.0.0.0/8,172.16.0.0/12}"
: "${SURICATA_LOG_MAX_MB:=512}"
: "${FORENSIC_PCAP_MODE:=blocks}"
: "${FORENSIC_PCAP_MODE:=alerts}"
: "${FORENSIC_PCAP_WINDOW_SECONDS:=60}"
: "${FORENSIC_PCAP_MEMORY_MB:=64}"
: "${FORENSIC_PCAP_MAX_FILES:=32}"
@@ -126,9 +142,6 @@ need scp
case "$ROUTER_PORT" in
*[!0-9]*|'') echo "ROUTER_PORT must be numeric" >&2; exit 2 ;;
esac
case "$VLAN_ID" in
*[!0-9]*|'') echo "VLAN_ID must be numeric" >&2; exit 2 ;;
esac
case "$TZSP_PORT" in
*[!0-9]*|'') echo "TZSP_PORT must be numeric" >&2; exit 2 ;;
esac
@@ -136,6 +149,17 @@ case "$FORENSIC_PCAP_MODE" in
blocks|alerts|all|off) ;;
*) echo "FORENSIC_PCAP_MODE must be one of: blocks, alerts, all, off" >&2; exit 2 ;;
esac
for bool_pair in \
"CONFIGURE_TZSP_CAPTURE=$CONFIGURE_TZSP_CAPTURE" \
"CONFIGURE_IPV4_MANGLE=$CONFIGURE_IPV4_MANGLE" \
"CONFIGURE_L2_SNIFFER=$CONFIGURE_L2_SNIFFER" \
"START_L2_SNIFFER=$START_L2_SNIFFER"
do
case "${bool_pair#*=}" in
true|false) ;;
*) echo "${bool_pair%%=*} must be true or false" >&2; exit 2 ;;
esac
done
if { [ -n "$METRICS_BASIC_AUTH_USERNAME" ] && [ -z "$METRICS_BASIC_AUTH_PASSWORD" ]; } || \
{ [ -z "$METRICS_BASIC_AUTH_USERNAME" ] && [ -n "$METRICS_BASIC_AUTH_PASSWORD" ]; }; then
echo "METRICS_BASIC_AUTH_USERNAME and METRICS_BASIC_AUTH_PASSWORD must both be set or both be empty" >&2
@@ -143,6 +167,10 @@ if { [ -n "$METRICS_BASIC_AUTH_USERNAME" ] && [ -z "$METRICS_BASIC_AUTH_PASSWORD
fi
for numeric_pair in \
"REDIS_PORT=$REDIS_PORT" \
"TZSP_RCVBUF_BYTES=$TZSP_RCVBUF_BYTES" \
"TZSP_BATCH_SIZE=$TZSP_BATCH_SIZE" \
"TZSP_QUEUE_MB=$TZSP_QUEUE_MB" \
"TZSP_DATAGRAM_BYTES=$TZSP_DATAGRAM_BYTES" \
"SURICATA_LOG_MAX_MB=$SURICATA_LOG_MAX_MB" \
"FORENSIC_PCAP_WINDOW_SECONDS=$FORENSIC_PCAP_WINDOW_SECONDS" \
"FORENSIC_PCAP_MEMORY_MB=$FORENSIC_PCAP_MEMORY_MB" \
@@ -193,6 +221,8 @@ for pair in \
"CONTAINER_SUBNET=$CONTAINER_SUBNET" \
"CONTAINER_BRIDGE=$CONTAINER_BRIDGE" \
"CONTAINER_VETH=$CONTAINER_VETH" \
"TZSP_L2_INTERFACE=$TZSP_L2_INTERFACE" \
"TZSP_L2_MAC_PROTOCOL=$TZSP_L2_MAC_PROTOCOL" \
"SURICATA_HOME_NET=$SURICATA_HOME_NET" \
"MONITORED_NETWORKS=$MONITORED_NETWORKS" \
"BLOCK_TIMEOUT=$BLOCK_TIMEOUT" \
@@ -248,10 +278,18 @@ if ! ssh_run "/file/print without-paging where name=\"${IMAGE_TAR_ROS}\"" | grep
echo "Upload it first with scripts/upload-routeros-image.sh." >&2
exit 5
fi
if [ "$CONFIGURE_SNIFFER" = "true" ]; then
if ! ssh_run '/tool/sniffer/print' >/dev/null; then
echo "RouterOS sniffer is unavailable." >&2
exit 4
if [ "$CONFIGURE_TZSP_CAPTURE" = "true" ]; then
if [ "$CONFIGURE_IPV4_MANGLE" = "true" ]; then
if ! ssh_run '/ip/firewall/mangle/print' >/dev/null; then
echo "RouterOS IPv4 mangle is unavailable." >&2
exit 4
fi
fi
if [ "$CONFIGURE_L2_SNIFFER" = "true" ]; then
if ! ssh_run '/tool/sniffer/print' >/dev/null; then
echo "RouterOS sniffer is unavailable." >&2
exit 4
fi
fi
fi
@@ -298,6 +336,10 @@ cat > "$LOCAL_RSC" <<RSC
/container/envs/remove [find where list="IDS_ENV"]
/container/envs/add list=IDS_ENV key=TZSP_BIND value="0.0.0.0"
/container/envs/add list=IDS_ENV key=TZSP_PORT value="${TZSP_PORT}"
/container/envs/add list=IDS_ENV key=TZSP_RCVBUF_BYTES value="${TZSP_RCVBUF_BYTES}"
/container/envs/add list=IDS_ENV key=TZSP_BATCH_SIZE value="${TZSP_BATCH_SIZE}"
/container/envs/add list=IDS_ENV key=TZSP_QUEUE_MB value="${TZSP_QUEUE_MB}"
/container/envs/add list=IDS_ENV key=TZSP_DATAGRAM_BYTES value="${TZSP_DATAGRAM_BYTES}"
/container/envs/add list=IDS_ENV key=TAP_NAME value="suritap0"
/container/envs/add list=IDS_ENV key=TAP_MTU value="9000"
/container/envs/add list=IDS_ENV key=SURICATA_HOME_NET value="${SURICATA_HOME_NET}"
@@ -410,16 +452,46 @@ cat >> "$LOCAL_RSC" <<RSC
:delay 5s
RSC
if [ "$CONFIGURE_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<RSC
if [ "$CONFIGURE_TZSP_CAPTURE" = "true" ]; then
if [ "$CONFIGURE_IPV4_MANGLE" = "true" ]; then
cat >> "$LOCAL_RSC" <<RSC
/tool/sniffer/stop
/tool/sniffer/set filter-vlan=${VLAN_ID} filter-direction=any filter-stream=yes only-headers=no streaming-enabled=yes streaming-server="${CONTAINER_IP_ONLY}" streaming-port=${TZSP_PORT}
# High-throughput routed IPv4 capture. sniff-tzsp continues to the next mangle
# rule after cloning the packet, so existing mangle processing remains active.
/ip/firewall/mangle/remove [find where comment="MikroSuricata TZSP IPv4"]
:if ([:len [/ip/firewall/mangle/find]] > 0) do={
/ip/firewall/mangle/add chain=forward action=sniff-tzsp sniff-target="${CONTAINER_IP_ONLY}" sniff-target-port=${TZSP_PORT} comment="MikroSuricata TZSP IPv4" place-before=0
} else={
/ip/firewall/mangle/add chain=forward action=sniff-tzsp sniff-target="${CONTAINER_IP_ONLY}" sniff-target-port=${TZSP_PORT} comment="MikroSuricata TZSP IPv4"
}
RSC
if [ "$START_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<'RSC'
fi
if [ "$CONFIGURE_L2_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<RSC
# Low-volume complement for everything that is not IPv4 EtherType 0x0800.
# No bridge/interface name is assumed. Empty TZSP_L2_INTERFACE maps to RouterOS "all".
/tool/sniffer/stop
:if ("${TZSP_L2_INTERFACE}" != "") do={
:if ([:len [/interface/find where name="${TZSP_L2_INTERFACE}"]] = 0) do={
:error "TZSP_L2_INTERFACE not found: ${TZSP_L2_INTERFACE}"
}
}
/tool/sniffer/set only-headers=no max-packet-size=2048 streaming-enabled=yes filter-stream=yes filter-interface="${TZSP_L2_FILTER_INTERFACE}" filter-mac-address="" filter-src-mac-address="" filter-dst-mac-address="" filter-mac-protocol=${TZSP_L2_MAC_PROTOCOL} filter-ip-address="" filter-src-ip-address="" filter-dst-ip-address="" filter-ipv6-address="" filter-src-ipv6-address="" filter-dst-ipv6-address="" filter-ip-protocol="" filter-port="" filter-src-port="" filter-dst-port="" filter-vlan="" filter-cpu="" filter-size="" filter-direction=any filter-operator-between-entries=or
# RouterOS current syntax uses streaming-server=IP:PORT. Older releases used a
# separate streaming-port property, so retain a fallback without assuming either.
:do {
/tool/sniffer/set streaming-server=${CONTAINER_IP_ONLY}:${TZSP_PORT}
} on-error={
/tool/sniffer/set streaming-server=${CONTAINER_IP_ONLY} streaming-port=${TZSP_PORT}
}
RSC
if [ "$START_L2_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<'RSC'
/tool/sniffer/start
RSC
fi
fi
fi
+2 -1
View File
@@ -11,7 +11,8 @@ mkdir -p \
"$PERSIST_LOG_DIR" \
"$PERSIST_LIB_DIR/rules" \
"$PERSIST_STATE_DIR" \
/run/suricata
/run/suricata \
/run/mikrosuricata
if ! id -u suricata >/dev/null 2>&1 || ! getent group suricata >/dev/null 2>&1; then
echo "[entrypoint] FATAL: missing suricata user/group in the image; rebuild the image from the current Dockerfile" >&2
+1
View File
@@ -18,5 +18,6 @@ run() {
fi
}
run "/container/print detail where name=\"${CONTAINER_NAME}\""
run "/ip/firewall/mangle/print stats where comment=\"MikroSuricata TZSP IPv4\""
run "/tool/sniffer/print"
run "/log/print without-paging where message~\"suricata|TZSP|IDS\""