Skip to content
LinkState
Go back

From brittle netns shell to a clean lab harness

Introduction to Refactoring Ad‑Hoc Scripts

Ad‑hoc scripts for Linux network namespaces and veth pairs typically consist of a linear series of ip netns add, ip link add, and ip link set commands. They assume a clean host state, hard‑code interface names, and omit verification that objects exist after creation. When a step fails—e.g., the namespace already exists or a veth pair collides with an existing interface—the script either continues with stale state or aborts, leaving half‑created resources. The result is a brittle test harness that requires manual cleanup and makes CI runs non‑deterministic.

Importance of Idempotence in Test Harnesses

Idempotence guarantees that invoking the same operation multiple times yields the same end state. In a test harness this means:


Designing the Idempotent Test Harness

Determining Naming Rules for Consistency

Consistent naming eliminates collisions and makes introspection trivial. We adopt the following conventions:

These rules are deterministic, reversible, and easy to parse with simple shell utilities.

Implementing Topology Assertions for Validation

After creating the desired graph we run a set of assertions that confirm the operational state matches the intended topology:

  1. Namespace existenceip netns list contains every expected namespace.
  2. Veth pair existenceip link show lists each veth pair exactly once.
  3. Endpoint placement – each veth side is enslaved to the correct namespace (ip -n <ns> link show <veth-side>).
  4. Addressing (optional) – if IP addresses are assigned, verify with ip -n <ns> addr show <veth-side>.
  5. Connectivity (optional) – a quick ping -c 1 -W 1 between peers to confirm L2/L3 viability.

Assertions are implemented as functions that return a non‑zero exit code on mismatch; the harness aborts and triggers cleanup.

Ensuring Deterministic Teardown for Reliability

Teardown must remove every resource the harness possibly created, even if the script exited mid‑flow. We achieve this by:


Refactoring the Script

Converting Ad‑Hoc Script to Idempotent Operations

Replace linear ip netns add … blocks with functions that encapsulate the check‑then‑act pattern:

ns_ensure() {
    local ns=$1
    if ! ip netns list | grep -qx "$ns"; then
        ip netns add "$ns"
        echo "$ns" >>"$STATE_DIR/created"
    fi
}

Similarly for veth pairs:

veth_ensure() {
    local ns_a=$1 ns_b=$2
    local veth="veth_${ns_a}_${ns_b}"
    if ! ip link show "$veth" >/dev/null 2>&1; then
        ip link add "$veth" type veth peer name "veth_${ns_b}_${ns_a}"
        ip link set "$veth" netns "$ns_a"
        ip link set "veth_${ns_b}_${ns_a}" netns "$ns_b"
        echo "$veth" >>"$STATE_DIR/created"
    fi
}

Both functions are safe to call repeatedly; they only create when missing and record the resource for later teardown.

Integrating Topology Assertions and Naming Rules

A topology description can be kept as a simple associative array or a YAML/JSON file. For illustration we use a bash array:

declare -A TOPO=(
    ["client_0"]="server_0"
    ["server_0"]="client_0"
    ["router_0"]="client_1 server_1"
    ["client_1"]="router_0"
    ["server_1"]="router_0"
)

The harness iterates over the keys, builds namespace names via the naming rule, ensures namespaces, then ensures each veth pair only once (by checking that the pair has not already been recorded). After the creation loop we invoke assert_topology which walks the same data structure and validates the kernel state as described earlier.

Implementing Cleaner Recovery from Partial Failure

We wrap the main logic in a set -euo pipefail block and install a trap:

cleanup() {
    # Delete in reverse order to avoid dependencies
    if [[ -f "$STATE_DIR/created" ]]; then
        tac "$STATE_DIR/created" | while read -r line; do
            if [[ "$line" =~ ^veth_ ]]; then
                ip link delete "$line" 2>/dev/null || true
            else
                ip netns delete "$line" 2>/dev/null || true
            fi
        done
        rm -f "$STATE_DIR/created"
    fi
    rmdir "$STATE_DIR" 2>/dev/null || true
}
trap cleanup EXIT

If any command fails, the EXIT trap runs, reads the state file, and removes everything that was successfully created up to that point. Because the delete functions are idempotent, a second cleanup pass (e.g., manual rerun) is harmless.


Test Harness Implementation

Writing the Test Harness in a Scripting Language

We choose bash for its ubiquitous presence and direct access to ip netns commands. The harness consists of:

The entire script is under 150 lines, making it easy to audit and embed in CI pipelines.

Example Code: Creating Namespaces and Veth Pairs

#!/usr/bin/env bash
set -euo pipefail

PREFIX="test_ns"
STATE_DIR=$(mktemp -d)
LOCKFILE="/var/run/test_harness.lock"

# ----- helpers ---------------------------------------------------------
ns_name() {
    local role=$1 idx=$2
    printf "%s_%s_%02d" "$PREFIX" "$role" "$idx"
}

veth_name() {
    local ns_a=$1 ns_b=$2
    # sort to guarantee same name irrespective of order
    if [[ "$ns_a" < "$ns_b" ]]; then
        printf "veth_%s_%s" "$ns_a" "$ns_b"
    else
        printf "veth_%s_%s" "$ns_b" "$ns_a"
    fi
}

ns_ensure() {
    local ns=$1
    if ! ip netns list | grep -qx "$ns"; then
        ip netns add "$ns"
        echo "$ns" >>"$STATE_DIR/created"
    fi
}

veth_ensure() {
    local ns_a=$1 ns_b=$2
    local veth=$(veth_name "$ns_a" "$ns_b")
    if ! ip link show "$veth" >/dev/null 2>&1; then
        ip link add "$veth" type veth peer name "veth_${ns_b}_${ns_a}"
        ip link set "$veth" netns "$ns_a"
        ip link set "veth_${ns_b}_${ns_a}" netns "$ns_b"
        echo "$veth" >>"$STATE_DIR/created"
    fi
}

# ----- topology assertion -----------------------------------------------
assert_topology() {
    local -n topo=$1   # nameref to associative array passed by caller
    for ns in "${!topo[@]}"; do
        # namespace existence
        if ! ip netns list | grep -qx "$ns"; then
            echo "ERROR: namespace $ns missing" >&2
            return 1
        fi
        # each expected neighbor must have a veth pair
        for neighbor in ${topo[$ns]}; do
            local veth=$(veth_name "$ns" "$neighbor")
            if ! ip link show "$veth" >/dev/null 2>&1; then
                echo "ERROR: veth $veth (between $ns and $neighbor) missing" >&2
                return 1
            fi
            # check that each side is in the correct netns
            local peer="veth_${neighbor}_${ns}"
            if ! ip -n "$ns" link show "$veth" >/dev/null 2>&1; then
                echo "ERROR: $veth not present in namespace $ns" >&2
                return 1
            fi
            if ! ip -n "$neighbor" link show "$peer" >/dev/null 2>&1; then
                echo "ERROR: $peer not present in namespace $neighbor" >&2
                return 1
            fi
        done
    done
    return 0
}

# ----- cleanup ---------------------------------------------------------
cleanup() {
    if [[ -f "$STATE_DIR/created" ]]; then
        tac "$STATE_DIR/created" | while read -r line; do
            if [[ "$line" =~ ^veth_ ]]; then
                ip link delete "$line" 2>/dev/null || true
            else
                ip netns delete "$line" 2>/dev/null || true
            fi
        done
        rm -f "$STATE_DIR/created"
    fi
    rmdir "$STATE_DIR" 2>/dev/null || true
}
trap cleanup EXIT

# ----- main -----------------------------------------------------------
main() {
    # acquire lock to prevent concurrent runs
    exec 200>"$LOCKFILE"
    flock -n 200 || { echo "Another instance is running; aborting." >&2; exit 1; }

    # Example static topology; could be loaded from a file
    declare -A TOPO=(
        ["client_0"]="server_0"
        ["server_0"]="client_0"
        ["router_0"]="client_1 server_1"
        ["client_1"]="router_0"
        ["server_1"]="router_0"
    )

    # Ensure namespaces
    for role in client server router; do
        for idx in 0 1; do   # adjust range as needed
            ns_ensure "$(ns_name "$role" "$idx")"
        done
    done

    # Ensure veth pairs (only once per edge)
    declared=()
    for ns in "${!TOPO[@]}"; do
        for neighbor in ${TOPO[$ns]}; do
            # normalize order to avoid duplicate work
            if [[ "$ns" < "$neighbor" ]]; then
                pair=("$ns" "$neighbor")
            else
                pair=("$neighbor" "$ns")
            fi
            # skip if already processed
            [[ " ${declared[*]} " =~ " ${pair[0]} ${pair[1]} " ]] && continue
            veth_ensure "${pair[0]}" "${pair[1]}"
            declared+=("${pair[0]} ${pair[1]}")
        done
    done

    # Validate
    assert_topology TOPO || {
        echo "Topology assertion failed" >&2
        exit 1
    }

    echo "Test harness ready. Namespaces and veth pairs created successfully."
    # Placeholder for user‑provided test logic
    # e.g., run a test suite here
}

main "$@"

Example Code: Asserting Topology and Cleaning Up

The assert_topology function shown above performs the validation. The cleanup trap guarantees that, on any exit path (including set -e triggered by a failed assertion), all tracked resources are removed. After a successful run the state file is empty; a subsequent run starts from a clean slate.


Troubleshooting Common Issues

Debugging Namespace and Veth Creation Failures

Handling Partial Failure and Recovery Scenarios

If the script dies midway through the veth creation loop, the state file contains only the resources successfully created. The EXIT trap runs cleanup, which reads the state file in reverse and deletes veth pairs before namespaces. Because each delete checks existence, a second pass is safe. To test this, you can insert return 1 or false at any point in the creation loop and observe that the trap removes everything created up to that point.


End of document.


Share this post on:

Previous Post
Telemetry architecture for containing cascades
Next Post
eBPF tracing for resolver path forensics