#!/usr/bin/env bash # scripts/verify-netem.sh — confirm tc-netem is actually applying loss, in # the direction(s) you think it is. # # Usage: # ./scripts/verify-netem.sh [interface] [loss_pct] # # peer-ip IP of the other machine (the simulator's IP when run on the CM5, # or the CM5's IP when run on the Mac). # interface Interface tc-netem is applied to. Default: eth0. # loss_pct Loss percentage to apply. Default: 5. # # Modes (env var): # BIDI=0 (default) Egress-only. Shapes outgoing traffic from . # Use a single ping from this host to verify. # BIDI=1 Bidirectional via ifb ingress redirect. Shapes BOTH # outgoing AND incoming traffic on . Pings # run from THIS host verify egress; the script also # prompts you to ping back FROM THE PEER to verify # ingress (the script holds the qdisc up until you press # Enter, then tears everything down). # # What it does: # 1. Prints the current qdisc state (sanity check before). # 2. Applies the configured netem loss (egress, or egress + ingress). # 3. Re-prints the qdisc state (confirms the rule is installed). # 4. Sends 100 ICMP echo requests to and reports the observed loss. # 5. BIDI=1 only: waits for you to run `ping -c 100 ` from the # peer machine and report what it saw. # 6. Removes the qdiscs (and brings ifb0 down) on exit, even on Ctrl-C. set -euo pipefail PEER="${1:-}" IFACE="${2:-eth0}" LOSS="${3:-5}" BIDI="${BIDI:-0}" IFB_DEV="${IFB_DEV:-ifb0}" if [[ -z "$PEER" ]]; then echo "Usage: $0 [interface] [loss_pct]" echo "Example: $0 192.168.1.42 eth0 5" exit 1 fi if [[ -t 1 ]]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m' RED=$'\033[31m'; RESET=$'\033[0m' else BOLD=; DIM=; GREEN=; YELLOW=; RED=; RESET= fi step() { printf '\n%s» %s%s\n' "$BOLD" "$1" "$RESET"; } ok() { printf '%s ✓ %s%s\n' "$GREEN" "$1" "$RESET"; } warn() { printf '%s ! %s%s\n' "$YELLOW" "$1" "$RESET"; } fail() { printf '%s ✗ %s%s\n' "$RED" "$1" "$RESET"; } # Sanity: tc + ping + interface for cmd in tc ping ip; do command -v "$cmd" >/dev/null || { fail "missing: $cmd"; exit 1; } done ip link show "$IFACE" >/dev/null 2>&1 || { fail "interface $IFACE not found"; exit 1; } # Print the route to the peer so the user can see which iface the kernel # actually uses — if it's not $IFACE, the netem rule won't apply. step "Route to peer $PEER" ROUTE_OUT="$(ip route get "$PEER" 2>&1 || true)" printf ' %s\n' "$ROUTE_OUT" ROUTE_IFACE="$(echo "$ROUTE_OUT" | awk '/dev/ {for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}')" if [[ -n "$ROUTE_IFACE" && "$ROUTE_IFACE" != "$IFACE" ]]; then warn "kernel routes $PEER via '$ROUTE_IFACE' but you're shaping '$IFACE'." warn "the netem rule will have NO effect on this peer's traffic." fi # State BEFORE step "Current qdisc on $IFACE (before)" sudo tc qdisc show dev "$IFACE" | sed 's/^/ /' # If bidi, ensure ifb device is up before installing qdiscs. if [[ "$BIDI" -eq 1 ]]; then step "Bringing up $IFB_DEV (BIDI mode)" sudo modprobe ifb numifbs=1 2>/dev/null || true if ! ip link show "$IFB_DEV" >/dev/null 2>&1; then fail "ifb device $IFB_DEV not present after modprobe; cannot run BIDI mode" exit 1 fi sudo ip link set "$IFB_DEV" up ok "$IFB_DEV is up" fi # Apply netem if [[ "$BIDI" -eq 1 ]]; then step "Applying netem loss ${LOSS}% on $IFACE (egress + ingress via $IFB_DEV)" else step "Applying netem loss ${LOSS}% on $IFACE (egress only)" fi sudo tc qdisc del dev "$IFACE" root 2>/dev/null || true sudo tc qdisc del dev "$IFACE" ingress 2>/dev/null || true [[ "$BIDI" -eq 1 ]] && sudo tc qdisc del dev "$IFB_DEV" root 2>/dev/null || true sudo tc qdisc add dev "$IFACE" root netem loss "${LOSS}%" if [[ "$BIDI" -eq 1 ]]; then sudo tc qdisc add dev "$IFACE" handle ffff: ingress sudo tc filter add dev "$IFACE" parent ffff: protocol all u32 \ match u32 0 0 action mirred egress redirect dev "$IFB_DEV" sudo tc qdisc add dev "$IFB_DEV" root netem loss "${LOSS}%" fi ok "qdisc(s) installed" # Trap to clean up on any exit path cleanup() { step "Removing netem qdiscs" sudo tc qdisc del dev "$IFACE" root 2>/dev/null || true sudo tc qdisc del dev "$IFACE" ingress 2>/dev/null || true if [[ "$BIDI" -eq 1 ]]; then sudo tc qdisc del dev "$IFB_DEV" root 2>/dev/null || true sudo ip link set "$IFB_DEV" down 2>/dev/null || true fi ok "qdiscs removed; $IFACE back to default" } trap cleanup EXIT INT TERM # State AFTER install step "Current qdisc state (after netem applied)" sudo tc qdisc show dev "$IFACE" | sed 's/^/ /' if [[ "$BIDI" -eq 1 ]]; then sudo tc qdisc show dev "$IFB_DEV" | sed 's/^/ /' echo " filter on $IFACE ingress:" sudo tc filter show dev "$IFACE" parent ffff: 2>/dev/null | sed 's/^/ /' fi # Ping the peer and parse loss step "Pinging $PEER with 100 echoes (egress goes through netem)" PING_OUT="$(ping -c 100 -i 0.05 -W 1 "$PEER" 2>&1 || true)" echo "$PING_OUT" | tail -3 | sed 's/^/ /' # Parse "X% packet loss" — works on both Linux and macOS ping output. OBSERVED="$(echo "$PING_OUT" | grep -oE '[0-9.]+% packet loss' | head -1 | awk '{print $1}' | tr -d '%')" if [[ -z "$OBSERVED" ]]; then fail "could not parse ping output" exit 1 fi # Sanity bracket: configured loss is ±3 percentage points absolute is fine for n=100. ABS_DELTA=$(awk -v o="$OBSERVED" -v l="$LOSS" 'BEGIN{d=o-l; if(d<0)d=-d; printf "%.1f", d}') step "Result" printf ' configured: %s%%\n observed: %s%%\n |delta|: %s pp\n' "$LOSS" "$OBSERVED" "$ABS_DELTA" if awk -v o="$OBSERVED" -v l="$LOSS" 'BEGIN{exit !(o > l*0.4 && o < l*2.0 + 3)}'; then ok "loss is being applied in the egress direction of $IFACE" else fail "observed loss ($OBSERVED%) does not match configured ($LOSS%)" warn "either the qdisc isn't routing as expected, or the kernel's netem" warn "build doesn't include the loss module. Check 'modprobe sch_netem'." fi if [[ "$BIDI" -eq 1 ]]; then step "Now verify the INGRESS direction" THIS_IP="$(ip -4 addr show dev "$IFACE" | awk '/inet / {print $2}' | cut -d/ -f1 | head -1)" cat <} You should see ~${LOSS}% packet loss in the peer's ping output. That confirms ingress shaping is dropping packets arriving here on $IFACE. Press Enter when done to tear everything down. EOF read -r _