first commit

This commit is contained in:
Mateusz Gruszczyński
2026-08-13 15:58:52 +02:00
commit adfdb0b86c
100 changed files with 6216 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
#!/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_ARCH:=auto}"
: "${ROUTER_DISK:=disk1}"
: "${ROUTER_SCP_DIR:=$ROUTER_DISK}"
: "${CONTAINER_NAME:=suricata-ids}"
: "${CONTAINER_IP:=172.31.255.2/30}"
: "${CONTAINER_GATEWAY:=172.31.255.1}"
: "${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}"
: "${SURICATA_HOME_NET:=[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]}"
: "${MONITORED_NETWORKS:=192.168.100.0/24}"
: "${AUTO_BLOCK:=false}"
: "${AUTO_BLOCK_MAX_SEVERITY:=1}"
: "${BLOCK_TIMEOUT:=1h}"
: "${UPDATE_RULES_ON_START:=false}"
: "${CREATE_REST_USER:=false}"
: "${ENABLE_WWW_SSL:=false}"
: "${ROUTEROS_REST_USER:=suricata-api}"
: "${ROUTEROS_REST_PASSWORD:=CHANGE_ME}"
: "${ROUTEROS_VERIFY_TLS:=false}"
: "${ROUTEROS_ADDRESS_LIST:=IDS-BLOCK}"
: "${REPLACE_EXISTING:=true}"
: "${KEEP_REMOTE_TAR:=true}"
: "${ENGINE:=}"
need() {
command -v "$1" >/dev/null 2>&1 || {
echo "$1 is required" >&2
exit 2
}
}
need ssh
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
# Values are inserted into a RouterOS script inside quoted strings. Reject
# characters that would require RouterOS-specific escaping.
check_ros_value() {
label="$1"
value="$2"
case "$value" in
*'"'*|*'\\'*|*'$'*|*';'*|*'`'*)
echo "$label contains a character not supported by the deploy script: $value" >&2
exit 3
;;
esac
case "$value" in
*'\n'*|*'\r'*)
echo "$label contains a newline" >&2
exit 3
;;
esac
}
for pair in \
"ROUTER_DISK=$ROUTER_DISK" \
"CONTAINER_NAME=$CONTAINER_NAME" \
"CONTAINER_IP=$CONTAINER_IP" \
"CONTAINER_GATEWAY=$CONTAINER_GATEWAY" \
"CONTAINER_SUBNET=$CONTAINER_SUBNET" \
"CONTAINER_BRIDGE=$CONTAINER_BRIDGE" \
"CONTAINER_VETH=$CONTAINER_VETH" \
"SURICATA_HOME_NET=$SURICATA_HOME_NET" \
"MONITORED_NETWORKS=$MONITORED_NETWORKS" \
"BLOCK_TIMEOUT=$BLOCK_TIMEOUT" \
"ROUTEROS_REST_USER=$ROUTEROS_REST_USER" \
"ROUTEROS_REST_PASSWORD=$ROUTEROS_REST_PASSWORD" \
"ROUTEROS_ADDRESS_LIST=$ROUTEROS_ADDRESS_LIST"
do
check_ros_value "${pair%%=*}" "${pair#*=}"
done
SSH_TARGET="${ROUTER_USER}@${ROUTER_HOST}"
ssh_run() {
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
ssh -i "$ROUTER_IDENTITY_FILE" -p "$ROUTER_PORT" "$SSH_TARGET" "$1"
else
ssh -p "$ROUTER_PORT" "$SSH_TARGET" "$1"
fi
}
scp_put() {
src="$1"
dst="$2"
if [ -n "$ROUTER_IDENTITY_FILE" ]; then
scp -i "$ROUTER_IDENTITY_FILE" -P "$ROUTER_PORT" "$src" "${SSH_TARGET}:$dst"
else
scp -P "$ROUTER_PORT" "$src" "${SSH_TARGET}:$dst"
fi
}
echo "[deploy] RouterOS preflight"
if ! ssh_run '/container/print' >/dev/null; then
echo "RouterOS container menu is unavailable. Install the matching container package and enable container device-mode first." >&2
exit 4
fi
if [ "$CONFIGURE_SNIFFER" = "true" ]; then
if ! ssh_run '/tool/sniffer/print' >/dev/null; then
echo "RouterOS sniffer is unavailable. Check device-mode sniffer=yes before deployment." >&2
exit 4
fi
fi
if [ "$ROUTER_ARCH" = "auto" ]; then
echo "[deploy] detecting RouterOS architecture"
RESOURCE="$(ssh_run '/system/resource/print without-paging')"
DETECTED="$(printf '%s\n' "$RESOURCE" | awk -F: '/architecture-name/ {gsub(/[[:space:]]/, "", $2); print $2; exit}')"
case "$DETECTED" in
arm64) ROUTER_ARCH=arm64 ;;
x86_64|x86|amd64) ROUTER_ARCH=amd64 ;;
arm) ROUTER_ARCH=arm ;;
*)
echo "Unsupported or undetected RouterOS architecture: ${DETECTED:-unknown}" >&2
echo "Set ROUTER_ARCH manually to arm64, amd64 or arm." >&2
exit 4
;;
esac
echo "[deploy] RouterOS architecture: $DETECTED -> image target $ROUTER_ARCH"
fi
case "$ROUTER_ARCH" in
arm64|amd64|arm) ;;
*) echo "ROUTER_ARCH must be auto, arm64, amd64 or arm" >&2; exit 4 ;;
esac
if [ -n "$ENGINE" ]; then
ENGINE="$ENGINE" ./scripts/build-routeros.sh "$ROUTER_ARCH"
else
./scripts/build-routeros.sh "$ROUTER_ARCH"
fi
LOCAL_TAR="build/routeros-suricata-tzsp-${ROUTER_ARCH}.tar"
[ -f "$LOCAL_TAR" ] || { echo "Missing $LOCAL_TAR" >&2; exit 5; }
DEPLOY_ID="$(date -u +%Y%m%d%H%M%S)"
REMOTE_TAR_NAME="routeros-suricata-tzsp-${ROUTER_ARCH}-${DEPLOY_ID}.tar"
REMOTE_TAR_ROS="${ROUTER_DISK}/${REMOTE_TAR_NAME}"
REMOTE_TAR_SCP="${ROUTER_SCP_DIR%/}/${REMOTE_TAR_NAME}"
LOCAL_RSC="build/deploy-${DEPLOY_ID}.rsc"
REMOTE_RSC_NAME="deploy-${DEPLOY_ID}.rsc"
REMOTE_RSC_ROS="${ROUTER_DISK}/${REMOTE_RSC_NAME}"
REMOTE_RSC_SCP="${ROUTER_SCP_DIR%/}/${REMOTE_RSC_NAME}"
ROOT_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-${DEPLOY_ID}"
DATA_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-data"
LOG_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-logs"
RULES_DIR="${ROUTER_DISK}/containers/${CONTAINER_NAME}-rules"
REST_URL="https://${CONTAINER_GATEWAY}"
CONTAINER_IP_ONLY="${CONTAINER_IP%/*}"
if [ "$CREATE_REST_USER" = "true" ] && [ "$ROUTEROS_REST_PASSWORD" = "CHANGE_ME" ]; then
echo "CREATE_REST_USER=true requires a real ROUTEROS_REST_PASSWORD" >&2
exit 6
fi
cat > "$LOCAL_RSC" <<RSC
# Generated by scripts/deploy-routeros.sh at ${DEPLOY_ID} UTC.
# Image: ${REMOTE_TAR_ROS}
:if ([:len [/interface/bridge/find where name="${CONTAINER_BRIDGE}"]] = 0) do={
/interface/bridge/add name="${CONTAINER_BRIDGE}" comment="Suricata IDS container bridge"
}
:if ([:len [/ip/address/find where interface="${CONTAINER_BRIDGE}" and address="${CONTAINER_GATEWAY}/30"]] = 0) do={
/ip/address/add address="${CONTAINER_GATEWAY}/30" interface="${CONTAINER_BRIDGE}" comment="Suricata IDS gateway"
}
:if ([:len [/interface/veth/find where name="${CONTAINER_VETH}"]] = 0) do={
/interface/veth/add name="${CONTAINER_VETH}" address="${CONTAINER_IP}" gateway="${CONTAINER_GATEWAY}" comment="Suricata IDS container"
}
:if ([:len [/interface/bridge/port/find where bridge="${CONTAINER_BRIDGE}" and interface="${CONTAINER_VETH}"]] = 0) do={
/interface/bridge/port/add bridge="${CONTAINER_BRIDGE}" interface="${CONTAINER_VETH}"
}
:if ([:len [/ip/firewall/nat/find where comment="Suricata IDS outbound NAT"]] = 0) do={
/ip/firewall/nat/add chain=srcnat src-address="${CONTAINER_SUBNET}" action=masquerade comment="Suricata IDS outbound NAT"
}
/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=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}"
/container/envs/add list=IDS_ENV key=MONITORED_NETWORKS value="${MONITORED_NETWORKS}"
/container/envs/add list=IDS_ENV key=AUTO_BLOCK value="${AUTO_BLOCK}"
/container/envs/add list=IDS_ENV key=AUTO_BLOCK_MAX_SEVERITY value="${AUTO_BLOCK_MAX_SEVERITY}"
/container/envs/add list=IDS_ENV key=BLOCK_TIMEOUT value="${BLOCK_TIMEOUT}"
/container/envs/add list=IDS_ENV key=ROUTEROS_URL value="${REST_URL}"
/container/envs/add list=IDS_ENV key=ROUTEROS_USER value="${ROUTEROS_REST_USER}"
/container/envs/add list=IDS_ENV key=ROUTEROS_PASSWORD value="${ROUTEROS_REST_PASSWORD}"
/container/envs/add list=IDS_ENV key=ROUTEROS_VERIFY_TLS value="${ROUTEROS_VERIFY_TLS}"
/container/envs/add list=IDS_ENV key=ROUTEROS_ADDRESS_LIST value="${ROUTEROS_ADDRESS_LIST}"
/container/envs/add list=IDS_ENV key=UPDATE_RULES_ON_START value="${UPDATE_RULES_ON_START}"
/container/mounts/remove [find where list="IDS_MOUNTS"]
/container/mounts/add list=IDS_MOUNTS src="${DATA_DIR}" dst=/data
/container/mounts/add list=IDS_MOUNTS src="${LOG_DIR}" dst=/var/log/suricata
/container/mounts/add list=IDS_MOUNTS src="${RULES_DIR}" dst=/var/lib/suricata
RSC
if [ "$CREATE_REST_USER" = "true" ]; then
cat >> "$LOCAL_RSC" <<RSC
:if ([:len [/user/group/find where name="ids-rest"]] = 0) do={
/user/group/add name=ids-rest policy=read,write,rest-api comment="Suricata IDS REST group"
}
:if ([:len [/user/find where name="${ROUTEROS_REST_USER}"]] = 0) do={
/user/add name="${ROUTEROS_REST_USER}" group=ids-rest address="${CONTAINER_IP_ONLY}/32" password="${ROUTEROS_REST_PASSWORD}"
} else={
/user/set [find where name="${ROUTEROS_REST_USER}"] group=ids-rest address="${CONTAINER_IP_ONLY}/32" password="${ROUTEROS_REST_PASSWORD}"
}
RSC
fi
if [ "$ENABLE_WWW_SSL" = "true" ]; then
cat >> "$LOCAL_RSC" <<'RSC'
/ip/service/enable www-ssl
RSC
fi
cat >> "$LOCAL_RSC" <<RSC
:if ([:len [/ip/firewall/filter/find where comment="IDS-BLOCK source"]] = 0) do={
/ip/firewall/filter/add chain=forward action=drop src-address-list="${ROUTEROS_ADDRESS_LIST}" disabled=yes comment="IDS-BLOCK source"
}
:if ([:len [/ip/firewall/filter/find where comment="IDS-BLOCK destination"]] = 0) do={
/ip/firewall/filter/add chain=forward action=drop dst-address-list="${ROUTEROS_ADDRESS_LIST}" disabled=yes comment="IDS-BLOCK destination"
}
:local old [/container/find where name="${CONTAINER_NAME}"]
:if ([:len \$old] > 0) do={
RSC
if [ "$REPLACE_EXISTING" = "true" ]; then
cat >> "$LOCAL_RSC" <<'RSC'
:if ([/container/get $old status] = "running") do={
/container/stop $old
:delay 3s
}
/container/remove $old
RSC
else
cat >> "$LOCAL_RSC" <<'RSC'
:error "Container already exists and REPLACE_EXISTING=false"
RSC
fi
cat >> "$LOCAL_RSC" <<RSC
}
/container/add file="${REMOTE_TAR_ROS}" interface="${CONTAINER_VETH}" root-dir="${ROOT_DIR}" mountlists=IDS_MOUNTS envlist=IDS_ENV name="${CONTAINER_NAME}" start-on-boot=yes logging=yes
:local cid [/container/find where name="${CONTAINER_NAME}"]
:local tries 0
:while (\$tries < 180) do={
:if ([/container/get \$cid status] = "stopped") do={
:set tries 999
} else={
:delay 2s
:set tries (\$tries + 1)
}
}
:if ([/container/get \$cid status] != "stopped") do={
:error "Container image extraction did not reach stopped state"
}
/container/start \$cid
:delay 5s
RSC
if [ "$CONFIGURE_SNIFFER" = "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}
RSC
if [ "$START_SNIFFER" = "true" ]; then
cat >> "$LOCAL_RSC" <<'RSC'
/tool/sniffer/start
RSC
fi
fi
cat >> "$LOCAL_RSC" <<RSC
:log info "Suricata IDS deployment ${DEPLOY_ID}: container created and start requested"
/container/print detail where name="${CONTAINER_NAME}"
RSC
printf '[deploy] uploading image via SCP: %s -> %s:%s\n' "$LOCAL_TAR" "$SSH_TARGET" "$REMOTE_TAR_SCP"
scp_put "$LOCAL_TAR" "$REMOTE_TAR_SCP"
printf '[deploy] uploading RouterOS deployment script: %s\n' "$REMOTE_RSC_SCP"
scp_put "$LOCAL_RSC" "$REMOTE_RSC_SCP"
echo "[deploy] importing configuration and starting container"
ssh_run "/import file-name=\"${REMOTE_RSC_ROS}\""
if [ "$KEEP_REMOTE_TAR" != "true" ]; then
ssh_run "/file/remove [find where name=\"${REMOTE_TAR_ROS}\"]" || true
fi
# The generated RSC can contain the REST password, so remove it after import.
ssh_run "/file/remove [find where name=\"${REMOTE_RSC_ROS}\"]" || true
echo "[deploy] final status"
ssh_run "/container/print detail where name=\"${CONTAINER_NAME}\""
echo "[deploy] recent container log lines"
ssh_run "/log/print without-paging where message~\"suricata|TZSP|IDS\"" || true
echo "[deploy] done"
echo "Container IP: ${CONTAINER_IP_ONLY}"
echo "Dashboard: http://${CONTAINER_IP_ONLY}:8080/"
echo "AUTO_BLOCK=${AUTO_BLOCK}; firewall DROP rules remain disabled by design."