test: harden dev topology and integration gate

This commit is contained in:
DaLaw2 2026-05-05 23:59:27 +08:00
parent eee63a558a
commit 76572d323d
5 changed files with 1045 additions and 438 deletions

View File

@ -1,6 +1,6 @@
# NetGuardia
Inline network security platform built on eBPF/XDP. Runs four independent detectors (per-packet ML, temporal beaconing, graph correlation, Suricata) over the same data plane, fuses their verdicts, drives SOAR playbooks, and writes every decision into a WORM audit chain.
Inline network security platform built on eBPF/XDP. Combines ONNX-based ML, temporal beaconing, correlation heuristics, and Suricata `eve.json` alerts in one fusion path, drives SOAR playbooks, and writes decisions into a WORM audit chain.
## Stack
@ -8,7 +8,7 @@ Inline network security platform built on eBPF/XDP. Runs four independent detect
- **Detection** — Rust + tract-onnx for ML, custom temporal / graph engines, Suricata `eve.json` ingest
- **Control plane** — actix-web REST + WebSocket, SQLite + SQLCipher, argon2 / JWT / CSRF, per-playbook SOAR
- **Frontend** — Vue 3 + Pinia + Vue-i18n (en / zh-TW / zh-CN / ja)
- **Architecture** — hexagonal: `adapter/` · `core/` · `infrastructure/` · `interface/` · `model/`
- **Architecture** — hexagonal-ish Rust workspace: `domain/` · `interface/` · `core/` · `adapter/` · `infrastructure/`
## Screens

View File

@ -17,7 +17,6 @@ RUN dnf install -y epel-release && \
git \
gh \
vim \
openssh-server \
ethtool \
nodejs24 \
nodejs24-npm \
@ -39,9 +38,5 @@ RUN ln -s /usr/bin/node-24 /usr/local/bin/node && \
ln -s /usr/bin/npm-24 /usr/local/bin/npm && \
ln -s /usr/bin/npx-24 /usr/local/bin/npx
RUN echo 'root:@Server20040421@' | chpasswd && \
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
WORKDIR /root/NetGuardia
CMD sh -c "ssh-keygen -A && /usr/sbin/sshd && sleep infinity"
CMD ["sleep", "infinity"]

View File

@ -22,7 +22,6 @@ services:
mgmt-net:
ipv4_address: 10.10.3.10
ports:
- "2222:22"
- "8080:8080"
volumes:
- /home/dalaw2/NetGuardia:/root/NetGuardia:z

View File

@ -1,146 +1,465 @@
#!/bin/bash
set -e
#!/usr/bin/env bash
# Build the NetGuardia development containers and inline veth topology.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEPLOY_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
COMPOSE_FILE="$DEPLOY_DIR/compose/podman-compose.yml"
ROOT_DIR="$(cd "$DEPLOY_DIR/.." && pwd)"
BASE_COMPOSE_FILE="$DEPLOY_DIR/compose/podman-compose.yml"
COMPOSE_FILE="/tmp/netguardia-compose-$$.yml"
COMPOSE_PROJECT="compose"
LOG_FILE="/tmp/netguardia-dev-$(date +%Y%m%d-%H%M%S).log"
if command -v podman-compose &>/dev/null; then
COMPOSE="podman-compose -f $COMPOSE_FILE"
RT="podman"
elif command -v docker &>/dev/null && docker compose version &>/dev/null 2>&1; then
COMPOSE="docker compose -f $COMPOSE_FILE"
RT="docker"
else
echo "ERROR: No container runtime found"
exit 1
fi
VERBOSE=0
CLEANUP_FIRST=1
RT=""
declare -a RT_CMD=()
declare -a COMPOSE_CMD=()
echo "=== Runtime: $RT ==="
echo "=== Kernel: $(uname -r) ==="
echo ""
echo "=== Building containers ==="
$COMPOSE build
echo "=== Starting containers ==="
$COMPOSE up -d
echo ""
echo "=== Containers running ==="
$RT ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null || $RT ps
get_pid() {
$RT inspect --format '{{.State.Pid}}' "$1"
info() {
printf '[INFO] %s\n' "$*"
}
mkdir -p /var/run/netns
warn() {
printf '[WARN] %s\n' "$*" >&2
}
EXT_PID=$(get_pid external)
INT_PID=$(get_pid internal)
RTR_PID=$(get_pid router)
NG_PID=$(get_pid netguardia)
ln -sf /proc/$EXT_PID/ns/net /var/run/netns/external
ln -sf /proc/$INT_PID/ns/net /var/run/netns/internal
ln -sf /proc/$RTR_PID/ns/net /var/run/netns/router
ln -sf /proc/$NG_PID/ns/net /var/run/netns/netguardia
fatal() {
printf '[ERROR] %s\n' "$*" >&2
exit 1
}
# ============================================================
# Segment 1: external <-> router (10.10.1.0/24)
# Direct connection, no inspection needed
# ============================================================
echo ""
echo "=== Segment 1: external <-> router (10.10.1.0/24) ==="
usage() {
cat <<EOF
Usage: sudo bash deploy/scripts/dev.sh [--verbose] [--no-cleanup]
ip link add ext-eth0 type veth peer name rtr-ext
ip link set ext-eth0 netns external
ip link set rtr-ext netns router
Options:
--verbose Print compose build/up output in addition to writing the log.
--no-cleanup Skip the default preflight cleanup of old containers/veth links.
-h, --help Show this help.
EOF
}
ip netns exec external ip link set lo up
ip netns exec external ip link set ext-eth0 up
ip netns exec external ip addr add 10.10.1.2/24 dev ext-eth0
for i in 3 4 5 6 7; do
ip netns exec external ip addr add 10.10.1.${i}/24 dev ext-eth0
done
ip netns exec external ip route add default via 10.10.1.1
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--verbose)
VERBOSE=1
shift
;;
--no-cleanup)
CLEANUP_FIRST=0
shift
;;
-h|--help)
usage
exit 0
;;
*)
fatal "Unknown argument: $1"
;;
esac
done
}
ip netns exec router ip link set lo up
ip netns exec router ip link set rtr-ext up
ip netns exec router ip addr add 10.10.1.1/24 dev rtr-ext
cleanup_netns_links() {
rm -f \
/var/run/netns/external \
/var/run/netns/internal \
/var/run/netns/router \
/var/run/netns/netguardia
}
echo " external: ext-eth0 10.10.1.{2-7}/24, gw 10.10.1.1"
echo " router: rtr-ext 10.10.1.1/24"
cleanup_temp_files() {
cleanup_netns_links
rm -f "$COMPOSE_FILE"
}
# ============================================================
# Segment 2: router <-> netguardia <-> internal (10.10.2.0/24)
# NetGuardia inline: XDP on ng-ext (router side) and ng-int (internal side)
# No bridges, no inline veth pair — direct XSK forwarding
# ============================================================
echo ""
echo "=== Segment 2: router <-> [NetGuardia] <-> internal (10.10.2.0/24) ==="
trap cleanup_temp_files EXIT
# router <-> netguardia: ng-ext is the netguardia side
ip link add rtr-int type veth peer name ng-ext
ip link set rtr-int netns router
ip link set ng-ext netns netguardia
require_root() {
[[ "$(id -u)" -eq 0 ]] || fatal "dev.sh must run as root. Use: sudo bash deploy/scripts/dev.sh"
}
# netguardia <-> internal: ng-int is the netguardia side
ip link add int-eth0 type veth peer name ng-int
ip link set int-eth0 netns internal
ip link set ng-int netns netguardia
require_linux_host() {
[[ "$(uname -s)" == "Linux" ]] || fatal "dev.sh supports Linux hosts only."
if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then
fatal "WSL2 is not supported for this XDP/AF_XDP development topology."
fi
}
# Router internal side
ip netns exec router ip link set rtr-int up
ip netns exec router ip addr add 10.10.2.1/24 dev rtr-int
ip netns exec router sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
package_manager() {
if command -v dnf >/dev/null 2>&1; then
printf 'dnf'
elif command -v apt-get >/dev/null 2>&1; then
printf 'apt-get'
elif command -v zypper >/dev/null 2>&1; then
printf 'zypper'
elif command -v pacman >/dev/null 2>&1; then
printf 'pacman'
fi
}
# Internal container
ip netns exec internal ip link set lo up
ip netns exec internal ip link set int-eth0 up
ip netns exec internal ip addr add 10.10.2.2/24 dev int-eth0
for i in 3 4 5 6; do
ip netns exec internal ip addr add 10.10.2.${i}/24 dev int-eth0
done
ip netns exec internal ip route add default via 10.10.2.1
package_for_command() {
local manager="$1"
local command_name="$2"
# NetGuardia interfaces (no IP, transparent)
ip netns exec netguardia ip link set ng-ext up
ip netns exec netguardia ip link set ng-int up
case "$manager:$command_name" in
dnf:ip) printf 'iproute' ;;
dnf:ping) printf 'iputils' ;;
dnf:ethtool) printf 'ethtool' ;;
dnf:curl) printf 'curl' ;;
dnf:ln|dnf:mkdir|dnf:rm|dnf:uname) printf 'coreutils' ;;
apt-get:ip) printf 'iproute2' ;;
apt-get:ping) printf 'iputils-ping' ;;
apt-get:ethtool) printf 'ethtool' ;;
apt-get:curl) printf 'curl' ;;
apt-get:ln|apt-get:mkdir|apt-get:rm|apt-get:uname) printf 'coreutils' ;;
zypper:ip) printf 'iproute2' ;;
zypper:ping) printf 'iputils' ;;
zypper:ethtool) printf 'ethtool' ;;
zypper:curl) printf 'curl' ;;
zypper:ln|zypper:mkdir|zypper:rm|zypper:uname) printf 'coreutils' ;;
pacman:ip) printf 'iproute2' ;;
pacman:ping) printf 'iputils' ;;
pacman:ethtool) printf 'ethtool' ;;
pacman:curl) printf 'curl' ;;
pacman:ln|pacman:mkdir|pacman:rm|pacman:uname) printf 'coreutils' ;;
esac
}
# Disable checksum offload on ALL veth endpoints.
# AF_XDP TX bypasses the kernel stack, so checksums are not computed.
# Without this, TCP packets forwarded through XSK have bad checksums and get dropped.
ip netns exec router ethtool -K rtr-int tx off rx off 2>/dev/null || true
ip netns exec router ethtool -K rtr-ext tx off rx off 2>/dev/null || true
ip netns exec internal ethtool -K int-eth0 tx off rx off 2>/dev/null || true
ip netns exec external ethtool -K ext-eth0 tx off rx off 2>/dev/null || true
ip netns exec netguardia ethtool -K ng-ext tx off rx off 2>/dev/null || true
ip netns exec netguardia ethtool -K ng-int tx off rx off 2>/dev/null || true
append_unique() {
local value="$1"
shift
local existing
echo " router: rtr-int (10.10.2.1) <-> ng-ext (XDP ingress)"
echo " netguardia: ng-ext <-> [XSK forwarding] <-> ng-int"
echo " internal: int-eth0 (10.10.2.{2-6}) <-> ng-int (XDP egress)"
echo " checksum offload disabled on all veth endpoints"
for existing in "$@"; do
[[ "$existing" == "$value" ]] && return 1
done
return 0
}
# ============================================================
# Verify
# ============================================================
echo ""
echo "=== Interfaces inside netguardia ==="
ip netns exec netguardia ip -br link show
install_packages() {
local manager="$1"
shift
echo ""
echo "=== Testing connectivity ==="
case "$manager" in
dnf)
dnf install -y "$@"
;;
apt-get)
DEBIAN_FRONTEND=noninteractive apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y "$@"
;;
zypper)
zypper --non-interactive install "$@"
;;
pacman)
pacman -Sy --noconfirm "$@"
;;
*)
return 1
;;
esac
}
echo -n " external -> router: "
ip netns exec external ping -c 1 -W 2 10.10.1.1 >/dev/null 2>&1 && echo "OK" || echo "FAIL"
manual_install_command() {
local manager="$1"
shift
# Without net-guardia, traffic between router and internal won't pass
# because ng-ext/ng-int are just veth endpoints with no forwarding
echo -n " router -> internal: "
ip netns exec router ping -c 1 -W 2 10.10.2.2 >/dev/null 2>&1 && echo "OK" || echo "FAIL (expected - needs net-guardia)"
case "$manager" in
dnf) printf 'dnf install -y %s\n' "$*" ;;
apt-get) printf 'apt-get update && apt-get install -y %s\n' "$*" ;;
zypper) printf 'zypper --non-interactive install %s\n' "$*" ;;
pacman) printf 'pacman -Sy --noconfirm %s\n' "$*" ;;
*) printf 'Install packages manually: %s\n' "$*" ;;
esac
}
cat > /tmp/netguardia_interfaces.txt << IEOF
check_host_tools() {
local required_commands=(ip ping ethtool curl ln mkdir rm uname)
local missing_commands=()
local packages=()
local command_name manager package answer
for command_name in "${required_commands[@]}"; do
if ! command -v "$command_name" >/dev/null 2>&1; then
missing_commands+=("$command_name")
fi
done
if ((${#missing_commands[@]} == 0)); then
info "Host tools OK"
return 0
fi
manager="$(package_manager || true)"
[[ -n "$manager" ]] || fatal "Missing host commands: ${missing_commands[*]}. No supported package manager found."
for command_name in "${missing_commands[@]}"; do
package="$(package_for_command "$manager" "$command_name")"
[[ -n "$package" ]] || fatal "No package mapping for missing command '$command_name' on $manager."
if append_unique "$package" "${packages[@]}"; then
packages+=("$package")
fi
done
warn "Missing host commands: ${missing_commands[*]}"
warn "Package manager: $manager"
warn "Packages to install: ${packages[*]}"
if [[ ! -t 0 ]]; then
manual_install_command "$manager" "${packages[@]}" >&2
fatal "Non-interactive shell; refusing to install packages without consent."
fi
read -r -p "Install missing packages? [y/N] " answer
case "$answer" in
y|Y|yes|YES)
install_packages "$manager" "${packages[@]}"
;;
*)
manual_install_command "$manager" "${packages[@]}" >&2
fatal "Required host packages were not installed."
;;
esac
}
runtime_install_hint() {
cat >&2 <<'EOF'
Install a supported container runtime first.
Examples:
dnf install -y podman podman-compose
apt-get install -y podman podman-compose
apt-get install -y docker.io docker-compose-plugin
EOF
}
check_docker_supported() {
local context security_options operating_system
context="$(docker context show 2>/dev/null || true)"
if [[ "$context" == "desktop-linux" ]]; then
fatal "Docker Desktop is not supported for this XDP/netns topology."
fi
operating_system="$(docker info --format '{{.OperatingSystem}}' 2>/dev/null || true)"
if [[ "$operating_system" == *"Docker Desktop"* ]]; then
fatal "Docker Desktop is not supported for this XDP/netns topology."
fi
security_options="$(docker info --format '{{json .SecurityOptions}}' 2>/dev/null || true)"
if grep -qi rootless <<<"$security_options"; then
fatal "Rootless Docker is not supported for this privileged XDP/netns topology."
fi
}
detect_runtime() {
if command -v podman-compose >/dev/null 2>&1 && command -v podman >/dev/null 2>&1; then
RT="podman"
RT_CMD=(podman)
COMPOSE_CMD=(podman-compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE")
elif command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
check_docker_supported
RT="docker"
RT_CMD=(docker)
COMPOSE_CMD=(docker compose -p "$COMPOSE_PROJECT" -f "$COMPOSE_FILE")
else
runtime_install_hint
fatal "No supported runtime found. Need podman + podman-compose or docker + docker compose."
fi
info "Runtime: $RT"
}
generate_compose_file() {
local yaml_deploy
local yaml_root
[[ -f "$BASE_COMPOSE_FILE" ]] || fatal "Compose file not found: $BASE_COMPOSE_FILE"
yaml_deploy="${DEPLOY_DIR//\'/\'\'}"
yaml_root="${ROOT_DIR//\'/\'\'}"
: >"$COMPOSE_FILE"
while IFS= read -r line; do
case "$line" in
" context: ..")
printf " context: '%s'\n" "$yaml_deploy" >>"$COMPOSE_FILE"
;;
" - /home/dalaw2/NetGuardia:/root/NetGuardia:z")
printf " - '%s:/root/NetGuardia:z'\n" "$yaml_root" >>"$COMPOSE_FILE"
;;
*)
printf '%s\n' "$line" >>"$COMPOSE_FILE"
;;
esac
done <"$BASE_COMPOSE_FILE"
}
run_logged() {
local label="$1"
shift
info "$label"
if ((VERBOSE)); then
"$@" 2>&1 | tee -a "$LOG_FILE"
elif [[ -t 1 ]]; then
run_with_spinner "$label" "$@"
elif ! "$@" >>"$LOG_FILE" 2>&1; then
warn "$label failed. Last log lines:"
tail -n 80 "$LOG_FILE" >&2 || true
fatal "Full log: $LOG_FILE"
fi
}
run_with_spinner() {
local label="$1"
shift
local pid status
"$@" >>"$LOG_FILE" 2>&1 &
pid=$!
spinner "$pid" "$label"
set +e
wait "$pid"
status=$?
set -e
clear_spinner_line
if ((status != 0)); then
warn "$label failed. Last log lines:"
tail -n 80 "$LOG_FILE" >&2 || true
fatal "Full log: $LOG_FILE"
fi
}
spinner() {
local pid="$1"
local label="$2"
local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0
local frame
local started_at=$SECONDS
while kill -0 "$pid" 2>/dev/null; do
frame="${frames:i++%${#frames}:1}"
printf '\r%s %s... %02ds' "$frame" "$label" "$((SECONDS - started_at))"
sleep 0.12
done
}
clear_spinner_line() {
printf '\r\033[K'
}
runtime_rm_containers() {
"${RT_CMD[@]}" rm -f netguardia router external internal >/dev/null 2>&1 || true
}
delete_host_link() {
local link_name="$1"
ip link del "$link_name" >/dev/null 2>&1 || true
}
preflight_cleanup() {
((CLEANUP_FIRST)) || return 0
info "Cleaning old development topology"
runtime_rm_containers
cleanup_netns_links
delete_host_link ext-eth0
delete_host_link rtr-ext
delete_host_link rtr-int
delete_host_link ng-ext
delete_host_link int-eth0
delete_host_link ng-int
}
container_pid() {
"${RT_CMD[@]}" inspect --format '{{.State.Pid}}' "$1"
}
link_netns() {
local container="$1"
local pid
pid="$(container_pid "$container")"
[[ -n "$pid" && "$pid" != "0" ]] || fatal "Container '$container' is not running."
ln -sf "/proc/$pid/ns/net" "/var/run/netns/$container"
}
link_container_namespaces() {
mkdir -p /var/run/netns
link_netns external
link_netns internal
link_netns router
link_netns netguardia
}
netns() {
ip netns exec "$@"
}
disable_offload() {
local namespace="$1"
local interface="$2"
netns "$namespace" ethtool -K "$interface" tx off rx off >/dev/null 2>&1 || true
}
create_topology() {
info "Creating inline veth topology"
ip link add ext-eth0 type veth peer name rtr-ext
ip link set ext-eth0 netns external
ip link set rtr-ext netns router
netns external ip link set lo up
netns external ip link set ext-eth0 up
netns external ip addr add 10.10.1.2/24 dev ext-eth0
for i in 3 4 5 6 7; do
netns external ip addr add "10.10.1.$i/24" dev ext-eth0
done
netns external ip route replace default via 10.10.1.1
netns router ip link set lo up
netns router ip link set rtr-ext up
netns router ip addr add 10.10.1.1/24 dev rtr-ext
ip link add rtr-int type veth peer name ng-ext
ip link set rtr-int netns router
ip link set ng-ext netns netguardia
ip link add int-eth0 type veth peer name ng-int
ip link set int-eth0 netns internal
ip link set ng-int netns netguardia
netns router ip link set rtr-int up
netns router ip addr add 10.10.2.1/24 dev rtr-int
netns router sh -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'
netns internal ip link set lo up
netns internal ip link set int-eth0 up
netns internal ip addr add 10.10.2.2/24 dev int-eth0
for i in 3 4 5 6; do
netns internal ip addr add "10.10.2.$i/24" dev int-eth0
done
netns internal ip route replace default via 10.10.2.1
netns netguardia ip link set ng-ext up
netns netguardia ip link set ng-int up
disable_offload router rtr-int
disable_offload router rtr-ext
disable_offload internal int-eth0
disable_offload external ext-eth0
disable_offload netguardia ng-ext
disable_offload netguardia ng-int
}
write_interface_mapping() {
cat >/tmp/netguardia_interfaces.txt <<'IEOF'
# NetGuardia interface mapping - realistic inline deployment
# Router handles L3 (10.10.1.0/24 <-> 10.10.2.0/24)
# NetGuardia inline on 10.10.2.0/24 (no IP, no bridge)
@ -149,30 +468,54 @@ cat > /tmp/netguardia_interfaces.txt << IEOF
# XSK forwards packets: ng-ext RX -> ng-int TX and ng-int RX -> ng-ext TX
# Management: eth0 (10.10.3.10)
IEOF
$RT cp /tmp/netguardia_interfaces.txt netguardia:/root/NetGuardia/interfaces.txt 2>/dev/null || true
"${RT_CMD[@]}" cp /tmp/netguardia_interfaces.txt netguardia:/root/NetGuardia/interfaces.txt >/dev/null 2>&1 || true
}
rm -f /var/run/netns/external /var/run/netns/internal /var/run/netns/router /var/run/netns/netguardia
connectivity_check() {
local external_router="FAIL"
echo ""
echo "=========================================="
echo " NetGuardia realistic inline deployment!"
echo ""
echo " external (10.10.1.{2-7})"
echo " |"
echo " [router] 10.10.1.1 <-> 10.10.2.1"
echo " | rtr-int"
echo " |"
echo " ng-ext (no IP) <- XDP ingress"
echo " |"
echo " [net-guardia XSK]"
echo " |"
echo " ng-int (no IP) <- XDP egress"
echo " |"
echo " | int-eth0"
echo " internal (10.10.2.{2-6})"
echo ""
echo " All 10.10.2.0/24 traffic requires net-guardia!"
echo " Mgmt: 10.10.3.10"
echo " SSH: ssh -p 2222 root@<host-ip>"
echo " Web: http://<host-ip>:8080"
echo "=========================================="
if netns external ping -c 1 -W 2 10.10.1.1 >/dev/null 2>&1; then
external_router="OK"
fi
info "Connectivity: external -> router: $external_router"
}
print_summary() {
cat <<EOF
NetGuardia development topology is ready.
Mgmt: http://<host-ip>:8080
external (10.10.1.{2-7}) -> router -> ng-ext
ng-ext <-> net-guardia XSK <-> ng-int
ng-int -> internal (10.10.2.{2-6})
EOF
}
main() {
parse_args "$@"
require_root
require_linux_host
check_host_tools
generate_compose_file
detect_runtime
preflight_cleanup
: >"$LOG_FILE"
info "Compose log: $LOG_FILE"
run_logged "Building containers" "${COMPOSE_CMD[@]}" build
run_logged "Starting containers" "${COMPOSE_CMD[@]}" up -d
info "Containers running"
"${RT_CMD[@]}" ps --format "table {{.Names}}\t{{.Status}}" 2>/dev/null || "${RT_CMD[@]}" ps
link_container_namespaces
create_topology
write_interface_mapping
connectivity_check
print_summary
}
main "$@"

View File

@ -1,315 +1,585 @@
#!/bin/bash
# NetGuardia 功能測試腳本
# 測試XDP 封包轉發、Web API、ML 引擎
# 使用 graceful shutdown所有操作設有 timeout
#!/usr/bin/env bash
# NetGuardia eBPF end-to-end gate.
# Builds the dev topology with deploy/scripts/dev.sh, starts net-guardia,
# exercises real packet paths, and shuts the data plane down with SIGINT.
set -uo pipefail
set -Eeuo pipefail
TIMEOUT=10
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DEV_SCRIPT="$ROOT_DIR/deploy/scripts/dev.sh"
NG_API="http://10.10.3.10:8080"
ADMIN_USER="admin"
ADMIN_PASSWORD="E2eAdmin20040421!"
CSRF_TOKEN="e2e"
RUN_LOG="${RUN_LOG:-/tmp/netguardia-ebpf-e2e.log}"
START_TIMEOUT_SECS="${START_TIMEOUT_SECS:-240}"
POLL_INTERVAL_SECS=2
SUDO_PASSWORD=""
RUNTIME=""
NG_PID=""
TOKEN=""
PASS=0
FAIL=0
TESTS=()
declare -a FAILURES=()
read -rsp "sudo password: " SUDO_PASSWORD
echo
run_sudo() {
sudo "$@" 2>/dev/null
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' "$@"
}
detect_runtime() {
if run_sudo podman container exists netguardia >/dev/null 2>&1; then
RUNTIME="podman"
elif run_sudo docker container inspect netguardia >/dev/null 2>&1; then
RUNTIME="docker"
elif command -v podman >/dev/null 2>&1; then
RUNTIME="podman"
elif command -v docker >/dev/null 2>&1; then
RUNTIME="docker"
else
echo "No supported container runtime found" >&2
return 1
fi
}
container_exec() {
run_sudo "$RUNTIME" exec "$@"
}
ng_exec() {
run_sudo podman exec netguardia bash -c "$1"
container_exec netguardia bash -lc "$1"
}
external_exec() {
container_exec external bash -lc "$1"
}
internal_exec() {
container_exec internal bash -lc "$1"
}
router_exec() {
run_sudo podman exec router bash -c "$1"
container_exec router bash -lc "$1"
}
ext_exec() {
run_sudo podman exec external bash -c "$1"
container_pid() {
run_sudo "$RUNTIME" inspect --format '{{.State.Pid}}' "$1"
}
int_exec() {
run_sudo podman exec internal bash -c "$1"
host_netns_exec() {
local container="$1"
shift
local pid
pid="$(container_pid "$container")"
run_sudo nsenter -t "$pid" -n "$@"
}
test_result() {
pass() {
echo " PASS $1"
PASS=$((PASS + 1))
}
fail() {
echo " FAIL $1"
FAIL=$((FAIL + 1))
FAILURES+=("$1")
}
check() {
local name="$1"
local result="$2"
if [ "$result" -eq 0 ]; then
echo "$name"
PASS=$((PASS + 1))
shift
if "$@"; then
pass "$name"
else
echo "$name"
FAIL=$((FAIL + 1))
fail "$name"
fi
TESTS+=("$name:$result")
}
api_raw() {
local method="$1"
local path="$2"
local body="${3:-}"
local auth_args=()
if [[ -n "$TOKEN" ]]; then
auth_args=(-H "Authorization: Bearer $TOKEN")
case "$method" in
POST|PUT|DELETE|PATCH)
auth_args+=(-H "X-CSRF-Token: $CSRF_TOKEN")
;;
esac
fi
if [[ -n "$body" ]]; then
ng_exec "curl -fsS --max-time 20 -X '$method' '${NG_API}${path}' -H 'Content-Type: application/json' ${auth_args[*]@Q} -d '$body'"
else
ng_exec "curl -fsS --max-time 20 -X '$method' '${NG_API}${path}' ${auth_args[*]@Q}"
fi
}
api_expect_ok() {
local method="$1"
local path="$2"
local body="${3:-}"
api_raw "$method" "$path" "$body" >/dev/null
}
api_ignore() {
api_expect_ok "$@" >/dev/null 2>&1 || true
}
json_field() {
local field="$1"
python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get(sys.argv[1], ""))' "$field"
}
drop_counter() {
local field="$1"
api_raw GET /api/stats/drops | json_field "$field"
}
counter_increased() {
local field="$1"
local before="$2"
local after
after="$(drop_counter "$field")"
[[ "$after" =~ ^[0-9]+$ ]] && (( after > before ))
}
counter_unchanged() {
local field="$1"
local before="$2"
local after
after="$(drop_counter "$field")"
[[ "$after" =~ ^[0-9]+$ ]] && (( after == before ))
}
wait_for_log() {
local pattern="$1"
local deadline=$((SECONDS + START_TIMEOUT_SECS))
while (( SECONDS < deadline )); do
if grep -q "$pattern" "$RUN_LOG" 2>/dev/null; then
return 0
fi
sleep "$POLL_INTERVAL_SECS"
done
return 1
}
wait_for_http() {
local deadline=$((SECONDS + START_TIMEOUT_SECS))
while (( SECONDS < deadline )); do
if ng_exec "curl -fsS --max-time 2 '${NG_API}/api/setup/status' >/dev/null"; then
return 0
fi
sleep "$POLL_INTERVAL_SECS"
done
return 1
}
find_net_guardia_pids() {
# shellcheck disable=SC2016 # The script is evaluated inside the container.
ng_exec 'for p in /proc/[0-9]*/cmdline; do
cmd=$(tr "\0" " " < "$p" 2>/dev/null || true)
case "$cmd" in
"target/release/net-guardia "*|"target/release/net-guardia")
pid=${p#/proc/}; echo "${pid%/cmdline}"
;;
esac
done'
}
kill_existing_net_guardia() {
local pids
pids="$(find_net_guardia_pids || true)"
if [[ -n "$pids" ]]; then
ng_exec "kill -INT $pids || true"
sleep 3
fi
}
detach_xdp_links() {
ng_exec "ip link set dev ng-ext xdp off 2>/dev/null || true; ip link set dev ng-int xdp off 2>/dev/null || true"
}
remove_pinned_xsk_maps() {
ng_exec "rm -f /sys/fs/bpf/INGRESS_XSKS_MAP /sys/fs/bpf/EGRESS_XSKS_MAP"
}
cleanup_runtime_state() {
run_sudo rm -f "$ROOT_DIR"/net-guardia.db "$ROOT_DIR"/net-guardia.db-shm "$ROOT_DIR"/net-guardia.db-wal
remove_pinned_xsk_maps || true
}
cleanup_api_state() {
[[ -z "$TOKEN" ]] && return 0
api_ignore DELETE /api/acl/ipv4/source/blacklist '"10.10.1.5:0"'
api_ignore DELETE /api/acl/ipv4/source/whitelist '"10.10.1.5:0"'
api_ignore DELETE /api/acl/ipv6/source/blacklist '"[fd00:1::5]:0"'
api_ignore DELETE /api/filter/http/ipv4 '["10.10.2.2:80",["GET"]]'
api_ignore DELETE /api/filter/ssh/ipv4 '"10.10.2.2:22"'
api_ignore DELETE /api/filter/ssh/blacklist/ipv4 '"10.10.1.5"'
api_ignore DELETE /api/filter/ssh/whitelist/ipv4 '"10.10.1.2"'
api_ignore POST /api/filter/ssh/whitelist/disable
api_ignore DELETE /api/filter/dns/blacklist '{"domains":["evil.example.com"]}'
api_ignore DELETE /api/acl/geo/unblock '{"country_codes":["US","AU","DE","NL","GB","JP","TW"]}'
router_exec "for ip in 8.8.8.8 8.8.4.4 1.1.1.1 9.9.9.9 80.249.99.148 51.140.0.1 133.242.0.1 1.34.0.1; do ip addr del \"\$ip/32\" dev rtr-int 2>/dev/null || true; done" || true
api_ignore PUT /api/rate-limit/config '{"packet_rate":10000,"syn_rate":100,"udp_rate":5000,"dns_rate":200,"window_ns":1000000000}'
}
shutdown_net_guardia() {
local pids
local had_inner_process=0
pids="$(find_net_guardia_pids || true)"
if [[ -n "$pids" ]]; then
had_inner_process=1
ng_exec "kill -INT $pids || true"
local deadline=$((SECONDS + 30))
while (( SECONDS < deadline )); do
[[ -z "$(find_net_guardia_pids || true)" ]] && break
sleep 1
done
fi
if [[ -n "$NG_PID" ]]; then
if kill -0 "$NG_PID" 2>/dev/null; then
kill -INT "$NG_PID" 2>/dev/null || true
local host_deadline=$((SECONDS + 15))
while (( SECONDS < host_deadline )); do
kill -0 "$NG_PID" 2>/dev/null || break
sleep 1
done
if kill -0 "$NG_PID" 2>/dev/null \
&& (( had_inner_process == 0 )) \
&& ! ng_exec "bpftool net show | grep -Eq 'ng-ext|ng-int|net_guardia'"
then
kill -TERM "$NG_PID" 2>/dev/null || true
fi
fi
if kill -0 "$NG_PID" 2>/dev/null; then
echo " WARN net-guardia host runner still alive after SIGINT; leaving final failure to cleanup checks" >&2
else
wait "$NG_PID" 2>/dev/null || true
fi
NG_PID=""
fi
}
assert_clean_shutdown() {
local pids
pids="$(find_net_guardia_pids || true)"
[[ -z "$pids" ]] || return 1
! ng_exec "bpftool net show | grep -Eq 'ng-ext|ng-int|net_guardia|xdp.*id'"
}
cleanup() {
set +e
cleanup_api_state
shutdown_net_guardia
assert_clean_shutdown >/dev/null 2>&1 || true
}
trap cleanup EXIT
setup_ipv6_topology() {
external_exec "ip -6 addr add fd00:1::2/64 dev ext-eth0 2>/dev/null || true; ip -6 addr add fd00:1::5/64 dev ext-eth0 2>/dev/null || true; ip -6 route replace default via fd00:1::1"
router_exec "ip -6 addr add fd00:1::1/64 dev rtr-ext 2>/dev/null || true; ip -6 addr add fd00:2::1/64 dev rtr-int 2>/dev/null || true"
host_netns_exec router sh -c "echo 1 > /proc/sys/net/ipv6/conf/all/forwarding"
internal_exec "ip -6 addr add fd00:2::2/64 dev int-eth0 2>/dev/null || true; ip -6 route replace default via fd00:2::1"
}
start_internal_http() {
internal_exec "ssh-keygen -A >/dev/null 2>&1 || true; /usr/sbin/sshd 2>/dev/null || true; pkill -f 'python3 -m http.server 80' 2>/dev/null || true; cd /var/www/html && nohup python3 -m http.server 80 >/tmp/ng-http.log 2>&1 &"
}
start_net_guardia() {
: > "$RUN_LOG"
(
cd "$ROOT_DIR"
printf '%s\n' "$SUDO_PASSWORD" | sudo -S -p '' "$RUNTIME" exec -i netguardia cargo run --release --bin net-guardia
) >"$RUN_LOG" 2>&1 &
NG_PID=$!
}
complete_setup_if_needed() {
local status
status="$(ng_exec "curl -fsS --max-time 5 '${NG_API}/api/setup/status'")"
if grep -q '"setup_complete":true' <<<"$status"; then
return 0
fi
api_raw POST /api/setup/complete '{"ingress_interface":"ng-ext","egress_interface":"ng-int","admin_password":"'"$ADMIN_PASSWORD"'","http_port":8080}' >/dev/null
wait_for_log "Full system initialization complete"
}
login() {
local body token
body="$(api_raw POST /api/auth/login '{"username":"'"$ADMIN_USER"'","password":"'"$ADMIN_PASSWORD"'"}')"
token="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])' <<<"$body")"
[[ -n "$token" ]]
TOKEN="$token"
}
scapy_send_ipv4_options_tcp() {
external_exec "python3 - <<'PY'
from scapy.all import IP, TCP, Raw, send, conf
conf.verb = 0
pkt = IP(src='10.10.1.2', dst='10.10.2.2', options=b'\x01\x01\x00\x00')/TCP(sport=45678, dport=80, flags='PA')/Raw(b'GET / HTTP/1.0\r\n\r\n')
send(pkt, count=3, inter=0.05)
PY"
}
scapy_send_invalid_ipv4() {
internal_exec "python3 - <<'PY'
from scapy.all import Ether, sendp, conf
conf.verb = 0
# Ethernet + IPv4 version/IHL byte with invalid IHL=4, sent directly into ng-int.
pkt = Ether(type=0x0800) / bytes([0x44,0,0,20,0,0,0,0,64,6,0,0,10,10,1,2,10,10,2,2])
sendp(pkt, iface='int-eth0', count=3, inter=0.05)
PY"
}
tcp_connect_from_external() {
local src_ip="$1"
local dst="$2"
external_exec "timeout 5 curl -fsS --interface '$src_ip' '$dst' >/dev/null"
}
ping4_from_external() {
local src_ip="$1"
local dst_ip="$2"
external_exec "ping -c 2 -W 3 -I '$src_ip' '$dst_ip' >/dev/null 2>&1"
}
ping6_from_external() {
local src_ip="$1"
local dst_ip="$2"
external_exec "ping -6 -c 2 -W 3 -I '$src_ip' '$dst_ip' >/dev/null 2>&1"
}
expect_blocked() {
if "$@"; then
return 1
fi
return 0
}
send_burst() {
local kind="$1"
external_exec "python3 - '$kind' <<'PY'
import sys
from scapy.all import IP, ICMP, TCP, UDP, DNS, DNSQR, send, conf
conf.verb = 0
kind = sys.argv[1]
if kind == 'packet':
pkt = IP(src='10.10.1.5', dst='10.10.2.2')/ICMP()
elif kind == 'syn':
pkt = IP(src='10.10.1.5', dst='10.10.2.2')/TCP(sport=41000, dport=22, flags='S')
elif kind == 'udp':
pkt = IP(src='10.10.1.5', dst='10.10.2.2')/UDP(sport=41000, dport=9999)/b'x'
elif kind == 'dns':
pkt = IP(src='10.10.1.5', dst='10.10.2.2')/UDP(sport=41000, dport=53)/DNS(rd=1, qd=DNSQR(qname='rate.example.com'))
else:
raise SystemExit(2)
send(pkt, count=20, inter=0.01)
PY"
}
send_dns_query() {
local domain="$1"
external_exec "python3 - '$domain' <<'PY'
import sys
from scapy.all import IP, UDP, DNS, DNSQR, send, conf
conf.verb = 0
domain = sys.argv[1]
pkt = IP(src='10.10.1.5', dst='10.10.2.2')/UDP(sport=53000, dport=53)/DNS(rd=1, qd=DNSQR(qname=domain))
send(pkt, count=5, inter=0.05)
PY"
}
send_geo_packet() {
local source_ip="$1"
router_exec "ip addr add '$source_ip/32' dev rtr-int 2>/dev/null || true; ping -c 5 -W 1 -I '$source_ip' 10.10.2.2 >/dev/null 2>&1 || true"
}
geo_block_prefixes() {
local country="$1"
local response
response="$(api_raw PUT /api/acl/geo/block '{"country_codes":["'"$country"'"]}')"
python3 -c 'import json,sys; print(json.load(sys.stdin).get("total_prefixes", 0))' <<<"$response"
}
geo_unblock_country() {
local country="$1"
api_expect_ok DELETE /api/acl/geo/unblock '{"country_codes":["'"$country"'"]}'
}
geo_block_counter_increases() {
local candidate country source_ip prefixes before
local candidates=(
"US 8.8.8.8"
"US 8.8.4.4"
"AU 1.1.1.1"
"DE 9.9.9.9"
"NL 80.249.99.148"
"GB 51.140.0.1"
"JP 133.242.0.1"
"TW 1.34.0.1"
)
for candidate in "${candidates[@]}"; do
country="${candidate%% *}"
source_ip="${candidate#* }"
geo_unblock_country "$country" >/dev/null 2>&1 || true
if ! prefixes="$(geo_block_prefixes "$country" 2>/dev/null)"; then
geo_unblock_country "$country" >/dev/null 2>&1 || true
continue
fi
if ! [[ "$prefixes" =~ ^[0-9]+$ ]] || (( prefixes == 0 )); then
geo_unblock_country "$country" >/dev/null 2>&1 || true
continue
fi
before="$(drop_counter geo_block)"
send_geo_packet "$source_ip"
sleep 1
if counter_increased geo_block "$before"; then
geo_unblock_country "$country" >/dev/null 2>&1 || true
return 0
fi
geo_unblock_country "$country" >/dev/null 2>&1 || true
done
return 1
}
run_rate_limit_case() {
local name="$1"
local field="$2"
local config="$3"
local burst_kind="$4"
local before
before="$(drop_counter "$field")"
api_expect_ok PUT /api/rate-limit/config "$config"
sleep 1
send_burst "$burst_kind"
sleep 1
check "$name" counter_increased "$field" "$before"
api_expect_ok PUT /api/rate-limit/config '{"packet_rate":10000,"syn_rate":100,"udp_rate":5000,"dns_rate":200,"window_ns":1000000000}'
}
echo "=========================================="
echo " NetGuardia 功能測試"
echo "=========================================="
echo ""
# ============================================================
# 1. 基礎檢查
# ============================================================
echo "--- 1. 基礎檢查 ---"
# 1a. 容器運行中
run_sudo podman ps --filter name=netguardia --format "{{.Status}}" | grep -q "Up" 2>/dev/null
test_result "netguardia 容器運行中" $?
# 1b. XDP 程式已附加
ng_exec "ip link show ng-ext 2>/dev/null | grep -q xdp"
test_result "ng-ext XDP 程式已附加" $?
ng_exec "ip link show ng-int 2>/dev/null | grep -q xdp"
test_result "ng-int XDP 程式已附加" $?
# 1c. 管理網路可達
ng_exec "ping -c1 -W $TIMEOUT 10.10.3.1 >/dev/null 2>&1"
test_result "管理網路 (10.10.3.1) 可達" $?
echo ""
# ============================================================
# 2. XDP 封包轉發測試
# ============================================================
echo "--- 2. XDP 封包轉發測試 ---"
# 2a. Router -> Internal (透過 NetGuardia)
router_exec "ping -c 3 -W $TIMEOUT 10.10.2.2 >/dev/null 2>&1"
test_result "Router → Internal ICMP 轉發 (10.10.2.2)" $?
# 2b. Internal -> Router (反向轉發)
int_exec "ping -c 3 -W $TIMEOUT 10.10.2.1 >/dev/null 2>&1"
test_result "Internal → Router ICMP 轉發 (10.10.2.1)" $?
# 2c. External -> Internal (全路徑: external -> router -> netguardia -> internal)
ext_exec "ping -c 3 -W $TIMEOUT 10.10.2.2 >/dev/null 2>&1"
test_result "External → Internal 全路徑 ICMP" $?
# 2d. Internal -> External (全路徑反向)
int_exec "ping -c 3 -W $TIMEOUT 10.10.1.2 >/dev/null 2>&1"
test_result "Internal → External 全路徑 ICMP" $?
# 2e. TCP 轉發 (HTTP)
ext_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' http://10.10.2.2/ 2>/dev/null" | grep -q "200"
test_result "External → Internal HTTP (TCP 轉發)" $?
echo ""
# ============================================================
# 3. Web API 測試
# ============================================================
echo "--- 3. Web API 測試 ---"
# 3a. Health endpoint
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/health/status" | grep -q "200"
test_result "GET /api/health/status" $?
# 3b. Health metrics
ng_exec "timeout $TIMEOUT curl -s $NG_API/api/health/metrics" | grep -q "cpu" 2>/dev/null
test_result "GET /api/health/metrics (含 CPU 資訊)" $?
# 3c. Flow stats
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/stats/flows" | grep -q "200"
test_result "GET /api/stats/flows" $?
# 3e. Stats summary
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/stats/summary" | grep -q "200"
test_result "GET /api/stats/summary" $?
# 3f. ML status
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/ml/status" | grep -q "200"
test_result "GET /api/ml/status" $?
# 3g. ACL list
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/acl/ipv4/source/whitelist" | grep -q "200"
test_result "GET /api/acl/ipv4/source/whitelist" $?
# 3h. Rate limit config
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/rate-limit/config" | grep -q "200"
test_result "GET /api/rate-limit/config" $?
echo ""
# ============================================================
# 4. ACL 功能測試
# ============================================================
echo "--- 4. ACL 功能測試 ---"
# 4a. 添加黑名單規則 (封鎖 10.10.1.5) — API 接收 SocketAddrV4 格式 "ip:port"
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/acl/ipv4/source/blacklist' -H 'Content-Type: application/json' -d '\"10.10.1.5:0\"'" | grep -q "200"
test_result "PUT ACL 黑名單規則 (封鎖 10.10.1.5)" $?
# 4b. 驗證封鎖生效 (10.10.1.5 不該能 ping 到 internal)
sleep 1
ext_exec "ping -c 2 -W 3 -I 10.10.1.5 10.10.2.2 >/dev/null 2>&1" && BLOCKED=1 || BLOCKED=0
test_result "10.10.1.5 被封鎖 (ping 失敗)" $BLOCKED
# 4c. 未封鎖的 IP 仍然可達
ext_exec "ping -c 2 -W $TIMEOUT -I 10.10.1.2 10.10.2.2 >/dev/null 2>&1"
test_result "10.10.1.2 未受影響 (ping 成功)" $?
# 4d. 移除黑名單規則
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/acl/ipv4/source/blacklist' -H 'Content-Type: application/json' -d '\"10.10.1.5:0\"'" | grep -q "200"
test_result "DELETE ACL 黑名單規則 (解除 10.10.1.5)" $?
# 4e. 驗證解除封鎖
sleep 1
ext_exec "ping -c 2 -W $TIMEOUT -I 10.10.1.5 10.10.2.2 >/dev/null 2>&1"
test_result "10.10.1.5 解除封鎖 (ping 恢復)" $?
echo ""
# ============================================================
# 5. Rate Limit 測試
# ============================================================
echo "--- 5. Rate Limit 測試 ---"
# 5a. 讀取當前 rate limit 設定
RL_CONFIG=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/rate-limit/config")
echo "$RL_CONFIG" | grep -q "packet_rate" 2>/dev/null
test_result "Rate limit 設定可讀取" $?
echo ""
# ============================================================
# 6. ML 引擎測試
# ============================================================
echo "--- 6. ML 引擎測試 ---"
ML_STATUS=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/ml/status")
echo "$ML_STATUS" | grep -q "active\|logging\|disabled" 2>/dev/null
test_result "ML 引擎狀態可查詢" $?
# 檢查 traffic log 是否在寫入 (traffic_logging_mode = true)
ng_exec "test -f /root/NetGuardia/traffic_log.csv && wc -l < /root/NetGuardia/traffic_log.csv || echo 0" | grep -qv "^0$" 2>/dev/null
test_result "Traffic log CSV 有寫入資料" $?
echo ""
# ============================================================
# 7. WebSocket 測試
# ============================================================
echo "--- 7. WebSocket 測試 ---"
# 簡單測試 WS endpoint 是否回應 (upgrade request)
# curl -sv 輸出 status code 到 stderr101 Switching Protocols 表示成功
WS_CODE=$(ng_exec "timeout 3 curl -s -o /dev/null -w '%{http_code}' -H 'Upgrade: websocket' -H 'Connection: Upgrade' -H 'Sec-WebSocket-Key: dGVzdA==' -H 'Sec-WebSocket-Version: 13' $NG_API/ws/health 2>/dev/null || true")
echo "$WS_CODE" | grep -q "101"
test_result "WebSocket /ws/health 升級成功 (101)" $?
echo ""
# ============================================================
# 8. GeoIP 國家封鎖 API 測試
# ============================================================
echo "--- 8. GeoIP 國家封鎖 API 測試 ---"
# 8a. 查詢目前封鎖的國家列表
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/acl/geo/blocked" | grep -q "200"
test_result "GET /api/acl/geo/blocked" $?
# 8b. 封鎖國家 (CN, RU) — GeoIP rebuild 需要掃描 MaxMind DBtimeout 加長到 30s
ng_exec "timeout 30 curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/acl/geo/block' -H 'Content-Type: application/json' -d '{\"country_codes\":[\"CN\",\"RU\"]}'" | grep -q "200"
test_result "PUT /api/acl/geo/block 封鎖 CN, RU" $?
# 8c. 驗證封鎖列表包含 CN
GEO_BLOCKED=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/acl/geo/blocked")
echo "$GEO_BLOCKED" | grep -q "CN" 2>/dev/null
test_result "封鎖列表包含 CN" $?
# 8d. 回傳包含 total_prefixes
GEO_PUT_RESP=$(ng_exec "timeout 30 curl -s -X PUT '$NG_API/api/acl/geo/block' -H 'Content-Type: application/json' -d '{\"country_codes\":[\"KP\"]}'")
echo "$GEO_PUT_RESP" | grep -q "total_prefixes" 2>/dev/null
test_result "PUT 回傳 total_prefixes 欄位" $?
# 8e. 解除封鎖
ng_exec "timeout 30 curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/acl/geo/unblock' -H 'Content-Type: application/json' -d '{\"country_codes\":[\"CN\",\"RU\",\"KP\"]}'" | grep -q "200"
test_result "DELETE /api/acl/geo/unblock 清除所有 GeoIP 規則" $?
# 8f. 驗證清空
GEO_AFTER=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/acl/geo/blocked")
echo "$GEO_AFTER" | grep -q '"blocked_countries":\[\]' 2>/dev/null || echo "$GEO_AFTER" | grep -q '"blocked_countries": \[\]' 2>/dev/null
test_result "封鎖列表已清空" $?
echo ""
# ============================================================
# 9. DNS 黑名單 API 測試
# ============================================================
echo "--- 9. DNS 黑名單 API 測試 ---"
# 9a. 查詢 DNS 黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' $NG_API/api/filter/dns/blacklist" | grep -q "200"
test_result "GET /api/filter/dns/blacklist" $?
# 9b. 新增域名到黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"malware.example.com\",\"phishing.test.org\"]}'" | grep -q "200"
test_result "PUT /api/filter/dns/blacklist 新增域名" $?
# 9c. 驗證黑名單已更新
DNS_DOMAINS=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/filter/dns/blacklist")
echo "$DNS_DOMAINS" | grep -q "malware.example.com" 2>/dev/null
test_result "黑名單包含 malware.example.com" $?
# 9d. 移除域名
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"phishing.test.org\"]}'" | grep -q "200"
test_result "DELETE 移除 phishing.test.org" $?
# 9e. 驗證移除結果
DNS_AFTER=$(ng_exec "timeout $TIMEOUT curl -s $NG_API/api/filter/dns/blacklist")
echo "$DNS_AFTER" | grep -q "malware.example.com" 2>/dev/null
test_result "移除後仍包含 malware.example.com" $?
# 9f. 清除所有 DNS 黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"malware.example.com\"]}'" | grep -q "200"
test_result "清除所有 DNS 黑名單規則" $?
echo ""
# ============================================================
# 10. DNS 黑名單封鎖流量驗證
# ============================================================
echo "--- 10. DNS 黑名單封鎖流量驗證 ---"
# 10a. 新增測試域名到黑名單
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X PUT '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"evil.example.com\"]}'" | grep -q "200"
test_result "新增 evil.example.com 到 DNS 黑名單" $?
# 10b. 對黑名單域名的 DNS 查詢應被丟棄 (timeout)
sleep 1
ext_exec "timeout 3 dig @10.10.2.2 evil.example.com +time=2 +tries=1 >/dev/null 2>&1" && DNS_BLOCKED=1 || DNS_BLOCKED=0
test_result "evil.example.com DNS 查詢被封鎖" $DNS_BLOCKED
# 10c. 子域名也應被封鎖
ext_exec "timeout 3 dig @10.10.2.2 sub.evil.example.com +time=2 +tries=1 >/dev/null 2>&1" && SUB_BLOCKED=1 || SUB_BLOCKED=0
test_result "sub.evil.example.com 子域名也被封鎖" $SUB_BLOCKED
# 10d. 清除
ng_exec "timeout $TIMEOUT curl -s -o /dev/null -w '%{http_code}' -X DELETE '$NG_API/api/filter/dns/blacklist' -H 'Content-Type: application/json' -d '{\"domains\":[\"evil.example.com\"]}'" | grep -q "200"
test_result "清除 DNS 黑名單測試規則" $?
echo ""
# ============================================================
# 結果總結
# ============================================================
echo "=========================================="
echo " 測試結果: $PASS 通過 / $FAIL 失敗 / $((PASS + FAIL)) 總計"
echo " NetGuardia eBPF E2E Gate"
echo "=========================================="
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "失敗的測試:"
for t in "${TESTS[@]}"; do
name="${t%:*}"
result="${t##*:}"
if [ "$result" -ne 0 ]; then
echo "$name"
fi
done
echo "--- Environment setup ---"
run_sudo "$DEV_SCRIPT"
detect_runtime
kill_existing_net_guardia
detach_xdp_links
cleanup_runtime_state
setup_ipv6_topology
start_internal_http
echo "--- Starting net-guardia ---"
start_net_guardia
check "setup/status becomes reachable" wait_for_http
check "setup completion starts full system" complete_setup_if_needed
check "ng-ext XDP attach logged" wait_for_log "XDP attached to ng-ext"
check "ng-int XDP attach logged" wait_for_log "XDP attached to ng-int"
check "XSK queue starts" wait_for_log "Queue pair 0 started successfully"
check "full system initialized" wait_for_log "Full system initialization complete"
check "login succeeds" login
echo "--- XSK forwarding ---"
check "IPv4 external to internal ICMP" ping4_from_external 10.10.1.2 10.10.2.2
check "IPv4 internal to external ICMP" internal_exec "ping -c 2 -W 3 10.10.1.2 >/dev/null 2>&1"
check "IPv4 external to internal HTTP" tcp_connect_from_external 10.10.1.2 http://10.10.2.2/
check "IPv6 external to internal ICMP" ping6_from_external fd00:1::2 fd00:2::2
echo "--- Parser packet path ---"
check "IPv4 IHL=6 TCP packet path does not detach" scapy_send_ipv4_options_tcp
sleep 1
check "Invalid IPv4 packet does not detach XDP" scapy_send_invalid_ipv4
check "XDP links remain attached after parser probes" ng_exec "bpftool net show | grep -Eq 'ng-ext|ng-int'"
echo "--- ACL ---"
acl_before="$(drop_counter acl_blacklist)"
api_expect_ok PUT /api/acl/ipv4/source/blacklist '"10.10.1.5:0"'
check "IPv4 source blacklist drops traffic" expect_blocked ping4_from_external 10.10.1.5 10.10.2.2
check "ACL blacklist counter increases" counter_increased acl_blacklist "$acl_before"
api_expect_ok PUT /api/acl/ipv4/source/whitelist '"10.10.1.5:0"'
check "Whitelist overrides blacklist" ping4_from_external 10.10.1.5 10.10.2.2
api_expect_ok DELETE /api/acl/ipv4/source/whitelist '"10.10.1.5:0"'
check "Blacklist resumes after whitelist removal" expect_blocked ping4_from_external 10.10.1.5 10.10.2.2
api_expect_ok DELETE /api/acl/ipv4/source/blacklist '"10.10.1.5:0"'
acl6_before="$(drop_counter acl_blacklist)"
api_expect_ok PUT /api/acl/ipv6/source/blacklist '"[fd00:1::5]:0"'
check "IPv6 source blacklist drops traffic" expect_blocked ping6_from_external fd00:1::5 fd00:2::2
check "IPv6 ACL counter increases" counter_increased acl_blacklist "$acl6_before"
api_expect_ok DELETE /api/acl/ipv6/source/blacklist '"[fd00:1::5]:0"'
echo "--- Protocol filter ---"
proto_before="$(drop_counter protocol_filter)"
api_expect_ok PUT /api/filter/http/ipv4 '["10.10.2.2:80",["GET"]]'
check "HTTP GET is allowed" external_exec "timeout 5 curl -fsS -X GET http://10.10.2.2/ >/dev/null"
check "HTTP POST is dropped" expect_blocked external_exec "timeout 5 curl -fsS -X POST http://10.10.2.2/ >/dev/null"
check "Protocol filter counter increases for POST" counter_increased protocol_filter "$proto_before"
api_expect_ok DELETE /api/filter/http/ipv4 '["10.10.2.2:80",["GET"]]'
proto_ssh_before="$(drop_counter protocol_filter)"
api_expect_ok PUT /api/filter/ssh/ipv4 '"10.10.2.2:22"'
api_expect_ok PUT /api/filter/ssh/blacklist/ipv4 '"10.10.1.5"'
check "SSH blacklist drops TCP connect" expect_blocked external_exec "timeout 5 nc -z -s 10.10.1.5 10.10.2.2 22"
check "SSH blacklist counter increases" counter_increased protocol_filter "$proto_ssh_before"
api_expect_ok DELETE /api/filter/ssh/blacklist/ipv4 '"10.10.1.5"'
api_expect_ok POST /api/filter/ssh/whitelist/enable
api_expect_ok PUT /api/filter/ssh/whitelist/ipv4 '"10.10.1.2"'
check "SSH whitelist allows listed source" external_exec "timeout 5 nc -z -s 10.10.1.2 10.10.2.2 22"
check "SSH whitelist blocks unlisted source" expect_blocked external_exec "timeout 5 nc -z -s 10.10.1.5 10.10.2.2 22"
api_expect_ok POST /api/filter/ssh/whitelist/disable
api_expect_ok DELETE /api/filter/ssh/whitelist/ipv4 '"10.10.1.2"'
api_expect_ok DELETE /api/filter/ssh/ipv4 '"10.10.2.2:22"'
echo "--- Rate limit ---"
run_rate_limit_case "Packet rate limit bucket" rate_limit_pkt '{"packet_rate":2,"syn_rate":1000,"udp_rate":1000,"dns_rate":1000,"window_ns":2000000000}' packet
run_rate_limit_case "SYN rate limit bucket" rate_limit_syn '{"packet_rate":1000,"syn_rate":2,"udp_rate":1000,"dns_rate":1000,"window_ns":2000000000}' syn
run_rate_limit_case "UDP rate limit bucket" rate_limit_udp '{"packet_rate":1000,"syn_rate":1000,"udp_rate":2,"dns_rate":1000,"window_ns":2000000000}' udp
run_rate_limit_case "DNS rate limit bucket" rate_limit_dns '{"packet_rate":1000,"syn_rate":1000,"udp_rate":1000,"dns_rate":2,"window_ns":2000000000}' dns
echo "--- DNS blacklist ---"
dns_before="$(drop_counter dns_blacklist)"
api_expect_ok PUT /api/filter/dns/blacklist '{"domains":["evil.example.com"]}'
send_dns_query evil.example.com
sleep 1
check "DNS blacklist counter increases" counter_increased dns_blacklist "$dns_before"
dns_allowed_before="$(drop_counter dns_blacklist)"
send_dns_query allowed.example.com
sleep 1
check "Non-blacklisted DNS does not increase DNS blacklist drops" counter_unchanged dns_blacklist "$dns_allowed_before"
api_expect_ok DELETE /api/filter/dns/blacklist '{"domains":["evil.example.com"]}'
echo "--- GeoIP block ---"
check "GeoIP block counter increases" geo_block_counter_increases
echo "--- Graceful shutdown ---"
cleanup_api_state
shutdown_net_guardia
check "No residual XDP link or net-guardia process" assert_clean_shutdown
trap - EXIT
echo "=========================================="
echo " eBPF E2E result: $PASS passed / $FAIL failed"
echo "=========================================="
if (( FAIL > 0 )); then
printf 'Failures:\n'
printf ' - %s\n' "${FAILURES[@]}"
echo "Run log: $RUN_LOG"
exit 1
fi
exit $FAIL
echo "Run log: $RUN_LOG"