#!/bin/bash # # init_eth_e2e_static_routes.sh # # Purpose: # Configure a *line* (daisy-chain) of Ethernet nodes where ALL nodes share one # logical IPv4 subnet (e.g., 10.10.0.0/24), but WITHOUT Proxy ARP or bridging. # We assign a /32 host address to *each* active interface and install explicit # static /32 routes per-destination to forward hop-by-hop via neighbors. # # Works for cabling like (no shared switch required): # # eth2 eth2 eth2 eth2 # apu00 ────┐ apu01 ────┐ apu02 ────┐ apu03 ────┐ apu04 # ┌───┘ ┌───┘ ┌───┘ ┌───┘ # eth1 eth1 eth1 eth1 # # Address plan (example with HOST_OCTET_BASE=10): # Node n → IP = 10.10.0.(10+n) # apu00 → 10.10.0.10, apu01 → 10.10.0.11, ..., apu24 → 10.10.0.34 # # Routing idea: # - Each node has SAME /32 local IP on eth1 and eth2 (allowed in Linux). # - For each *neighbor*, install an on-link /32 route: "dev ethX scope link". # - For farther nodes, install /32 routes "via dev ethX onlink". # - Enable ip_forward. Keep proxy_arp=0 (no ARP routing). # - ARP is used only to resolve immediate next-hop MAC on the wire. # # Notes: # - We intentionally avoid a loop-closure (first<->last) to keep routing simple. # If you need a measurement loop, use a separate isolated link. # - We do NOT touch your default route or other interfaces (e.g., eth0, mesh0). # # ------------------------------------------------------------------------------ ### === VALIDATE ARGUMENTS === if [[ $# -eq 0 ]]; then echo "Usage: $0 " echo "Example: $0 0 1 2 3 4 9 14 19 24" exit 1 fi NODES=("$@") ### === CONFIGURATION (EDIT AS NEEDED) === SUBNET_BASE="10.10.0" # A.B.C HOST_OCTET_BASE=10 # host octet = 10 + NODE_ID (apu00 → .10) IF_LEFT="eth1" # toward lower index (left) IF_RIGHT="eth2" # toward higher index (right) SET_MTU=1500 # MTU for both links SET_TXQUEUELEN=1000 # optional queue length tuning ### === GET SELF ID (robust for 'apu00', 'apu01', ...) === HOSTNAME=$(hostname) if [[ $HOSTNAME =~ ([0-9]+)$ ]]; then NODE_ID=$((10#${BASH_REMATCH[1]})) # base-10 (avoid octal) else echo "Cannot parse NODE_ID from hostname '$HOSTNAME'"; exit 1 fi echo "HostName: $HOSTNAME NodeID: $NODE_ID" ### === ASCII: NODE VIEW (example for a middle node) === # [apu01:10.10.0.11] <--eth1--> [apu02:10.10.0.12] <--eth2--> [apu03:10.10.0.13] # Both eth1 and eth2 on apu02 get the SAME local /32 (10.10.0.12/32). # Neighbor routes are 'scope link', farther routes are 'via onlink'. ### === FIND OUR INDEX IN THE CHAIN === NODE_INDEX=-1 for idx in "${!NODES[@]}"; do if [[ $((10#${NODES[$idx]})) -eq $NODE_ID ]]; then NODE_INDEX=$idx break fi done if [[ "$NODE_INDEX" -lt 0 ]]; then echo "Skip [ETH E2E static]: Node ID $NODE_ID not in list: ${NODES[*]}" exit 1 fi ### === COMPUTE SELF & NEIGHBORS === SELF_HOST_OCTET=$((HOST_OCTET_BASE + NODE_ID)) SELF_IP="${SUBNET_BASE}.${SELF_HOST_OCTET}" HAS_LEFT=0; HAS_RIGHT=0 LEFT_ID=; RIGHT_ID= LEFT_IP=; RIGHT_IP= if (( NODE_INDEX > 0 )); then HAS_LEFT=1 LEFT_ID=$((10#${NODES[$((NODE_INDEX-1))]})) LEFT_IP="${SUBNET_BASE}.$((HOST_OCTET_BASE + LEFT_ID))" fi if (( NODE_INDEX < ${#NODES[@]} - 1 )); then HAS_RIGHT=1 RIGHT_ID=$((10#${NODES[$((NODE_INDEX+1))]})) RIGHT_IP="${SUBNET_BASE}.$((HOST_OCTET_BASE + RIGHT_ID))" fi ### === SAFETY: BRING IFACES DOWN & FLUSH OLD STATE === for IF in "$IF_LEFT" "$IF_RIGHT"; do ip link set "$IF" down 2>/dev/null || true ip addr flush dev "$IF" || true ip route flush dev "$IF" || true ip neigh flush dev "$IF" || true done ### === BASE LINK SETTINGS === for IF in "$IF_LEFT" "$IF_RIGHT"; do ip link set "$IF" mtu "$SET_MTU" 2>/dev/null || true ip link set "$IF" txqueuelen "$SET_TXQUEUELEN" 2>/dev/null || true done ### === ASSIGN /32 ON ACTIVE INTERFACES (same IP on both) === if (( HAS_LEFT )); then ip addr add "${SELF_IP}/32" dev "$IF_LEFT" 2>/dev/null || true ip link set "$IF_LEFT" up fi if (( HAS_RIGHT )); then ip addr add "${SELF_IP}/32" dev "$IF_RIGHT" 2>/dev/null || true ip link set "$IF_RIGHT" up fi ### === SYSCTLS: PURE ROUTING, NO PROXY ARP === # Forward packets between interfaces sysctl -q -w net.ipv4.ip_forward=1 # Disable Proxy ARP; relax rp_filter (strict can drop forwarded traffic) for IF in "$IF_LEFT" "$IF_RIGHT"; do sysctl -q -w "net.ipv4.conf.${IF}.proxy_arp=0" sysctl -q -w "net.ipv4.conf.${IF}.rp_filter=0" done sysctl -q -w net.ipv4.conf.all.proxy_arp=0 sysctl -q -w net.ipv4.conf.all.rp_filter=0 # ARP hygiene (avoid ARP flux with same IP on multiple ifaces) for IF in "$IF_LEFT" "$IF_RIGHT"; do sysctl -q -w "net.ipv4.conf.${IF}.arp_filter=1" sysctl -q -w "net.ipv4.conf.${IF}.arp_ignore=1" sysctl -q -w "net.ipv4.conf.${IF}.arp_announce=2" done sysctl -q -w net.ipv4.conf.all.arp_filter=1 sysctl -q -w net.ipv4.conf.all.arp_ignore=1 sysctl -q -w net.ipv4.conf.all.arp_announce=2 ### === STATIC NEXT-HOP /32 ROUTES === # Strategy: # 1) Install neighbor /32 on-link routes (scope link) first. # 2) For every other node, install /32 via the proper neighbor with 'onlink'. # 3) Add 'src SELF_IP' so replies have a consistent source. # # Neighbor routes: if (( HAS_LEFT )); then ip route replace "${LEFT_IP}/32" dev "$IF_LEFT" scope link src "${SELF_IP}" fi if (( HAS_RIGHT )); then ip route replace "${RIGHT_IP}/32" dev "$IF_RIGHT" scope link src "${SELF_IP}" fi # Farther routes: for target_idx in "${!NODES[@]}"; do TID=$((10#${NODES[$target_idx]})) [[ "$TID" -eq "$NODE_ID" ]] && continue TARGET_IP="${SUBNET_BASE}.$((HOST_OCTET_BASE + TID))" if (( target_idx < NODE_INDEX )); then # Target is to the LEFT if (( HAS_LEFT )); then [[ -n "$LEFT_ID" && "$TID" -eq "$LEFT_ID" ]] && continue # skip neighbor (already scope link) ip route replace "${TARGET_IP}/32" via "${LEFT_IP}" dev "$IF_LEFT" onlink src "${SELF_IP}" fi else # Target is to the RIGHT if (( HAS_RIGHT )); then [[ -n "$RIGHT_ID" && "$TID" -eq "$RIGHT_ID" ]] && continue # skip neighbor (already scope link) ip route replace "${TARGET_IP}/32" via "${RIGHT_IP}" dev "$IF_RIGHT" onlink src "${SELF_IP}" fi fi done ### === SUMMARY === LEFT_SHOW="-" ; RIGHT_SHOW="-" (( HAS_LEFT )) && LEFT_SHOW="${LEFT_ID} (${LEFT_IP})" (( HAS_RIGHT )) && RIGHT_SHOW="${RIGHT_ID} (${RIGHT_IP})" echo echo "=== ETH E2E STATIC-ROUTE CONFIGURATION ===" echo "HOSTNAME : $HOSTNAME" echo "NODE_ID : $NODE_ID" echo "SELF_IP (/32) : $SELF_IP" echo "NODES : ${NODES[*]}" echo "LEFT NEIGHBOR : $LEFT_SHOW" echo "RIGHT NEIGHBOR : $RIGHT_SHOW" echo "IF UP : $([[ $HAS_LEFT -eq 1 ]] && echo $IF_LEFT) $([[ $HAS_RIGHT -eq 1 ]] && echo $IF_RIGHT)" echo "IP FORWARDING : $(sysctl -n net.ipv4.ip_forward)" echo "Proxy ARP ($IF_LEFT): $(sysctl -n net.ipv4.conf.${IF_LEFT}.proxy_arp 2>/dev/null || echo N/A)" echo "Proxy ARP ($IF_RIGHT): $(sysctl -n net.ipv4.conf.${IF_RIGHT}.proxy_arp 2>/dev/null || echo N/A)" echo "ARP hardening (all) : filter=$(sysctl -n net.ipv4.conf.all.arp_filter), ignore=$(sysctl -n net.ipv4.conf.all.arp_ignore), announce=$(sysctl -n net.ipv4.conf.all.arp_announce)" echo "==========================================" echo cat <<'ASCII' Quick test (from an edge like apu00): ip route show | grep 10.10.0. ping -c 2 10.10.0. # e.g., 10 ping -c 2 10.10.0. # e.g., 34 If only neighbors respond, check: sysctl net.ipv4.ip_forward # must be 1 nft list ruleset | grep -i forward # firewall may block FORWARD iptables -S FORWARD # policy should ACCEPT or allow eth1<->eth2 sysctl net.ipv4.conf.eth1.rp_filter # should be 0 sysctl net.ipv4.conf.eth2.rp_filter # should be 0 ip -4 neigh show dev eth1; ip -4 neigh show dev eth2 ASCII echo "[${HOSTNAME}] E2E static-route configuration complete."