1- setup-multi-nic-routing.sh #! /usr/bin/env bash
1+ #! /usr/bin/env bash
22#
33# setup-multi-nic-routing.sh
4- # v1.0
54#
6- # Configures source-based routing for multiple NICs using NetworkManager.
5+ # This script configures source-based routing for multiple NICs using NetworkManager.
76#
8- # Features :
9- # - Validates NICs are present and up
10- # - Creates dedicated routing tables: weka-<nic>
11- # - Adds source-based routing rules
12- # - Routes local subnet and optional client subnet via NIC
13- # - Enables ARP tuning (arp_filter/arp_announce)
14- # - Sets MTU=9000 for Ethernet interfaces
15- # - --reset: removes rules/routes only, preserves NM connections
16- # - Supports multi-run, multi-NIC, different subnets safely
7+ # For each specified NIC :
8+ # - Validates the interface is present and up
9+ # - Retrieves its IPv4 address and subnet
10+ # - Creates a dedicated routing table (e.g. weka-eth0, weka-eth1, ...)
11+ # - Adds a source-based routing rule for traffic from that IP
12+ # - Adds a route to the local subnet via the same interface and IP
13+ # - Optionally adds a route to a remote client subnet via a specified gateway
14+ # - Enables ARP tuning (arp_filter=1, arp_announce=2) only for the listed NICs
15+ # - Sets MTU=9000 for Ethernet interfaces (unless --no-mtu is used)
1716#
18- # Usage examples:
19- # ./setup-multi-nic-routing.sh --nics ib0 ib1
17+ # Supports:
18+ # --nics Space-separated list of NICs (required)
19+ # --client-subnet Optional remote subnet (CIDR format) to route via gateway (could be 0.0.0.0/0)
20+ # --gateway Optional gateway for the client subnet
21+ # --reset Removes any existing source-based routing rules created by this script
22+ # --no-mtu Skip setting MTU=9000 for Ethernet NICs
23+ # --dry-run Show intended actions without applying them
24+ #
25+ # Example usage:
2026# ./setup-multi-nic-routing.sh --nics eth0 eth1 --client-subnet 192.168.50.0/24 --gateway 10.0.0.254
21- # ./setup-multi-nic-routing.sh --nics ens19 ens20 --reset
22- # ./setup-multi-nic-routing.sh --nics eth0 --client-subnet 0.0.0.0/0 --gateway 10.1.1.254
23- # ./setup-multi-nic-routing.sh --nics eth1 --client-subnet 0.0.0.0/0 --gateway 10.1.2.254
27+ # ./setup-multi-nic-routing.sh --nics eth0 eth1 --client-subnet 0.0.0.0/0 --gateway 10.1.1.254
28+ # ./setup-multi-nic-routing.sh --nics ib0 ib1 # Flat IB or Ethernet (no gateway)
29+ # ./setup-multi-nic-routing.sh --nics eth0 eth1 --reset # Remove only routing rules
30+ # ./setup-multi-nic-routing.sh --nics eth0 eth1 --no-mtu # Skip MTU 9000 change
31+ # ./setup-multi-nic-routing.sh --nics eth0 eth1 --dry-run # Preview only
2432#
2533
2634set -euo pipefail
@@ -30,12 +38,13 @@ ROUTE_TABLE_PREFIX="weka"
3038SYSCTL_FILE=" /etc/sysctl.d/99-weka.conf"
3139RT_TABLES=" /etc/iproute2/rt_tables"
3240DRY_RUN=false
33- RESET=false
41+ RESET_MODE=false
42+ NO_MTU=false
3443
3544# ---- FUNCTIONS ----
3645
3746usage () {
38- echo " Usage: $0 --nics <nic1> [nic2 ...] [--client-subnet <CIDR>] [--gateway <IP>] [--dry-run ] [--reset ]"
47+ echo " Usage: $0 --nics <nic1> [nic2 ...] [--client-subnet <CIDR>] [--gateway <IP>] [--reset] [--no-mtu ] [--dry-run ]"
3948 exit 1
4049}
4150
@@ -69,9 +78,15 @@ get_ip_for_nic() {
6978
7079get_network_cidr () {
7180 local ip_cidr=" $1 "
72- local network_line
73- network_line=$( ipcalc " $ip_cidr " 2> /dev/null | awk ' /^Network:/ {print $2}' )
74- echo " $network_line "
81+ ipcalc " $ip_cidr " 2> /dev/null | awk ' /^Network:/ {print $2}'
82+ }
83+
84+ ensure_rt_table () {
85+ local table_id=" $1 "
86+ local table_name=" $2 "
87+ if ! grep -qw " $table_name " " $RT_TABLES " ; then
88+ run_or_echo " echo \" $table_id $table_name \" >> \" $RT_TABLES \" "
89+ fi
7590}
7691
7792apply_sysctl_arp_settings () {
@@ -91,55 +106,27 @@ apply_sysctl_arp_settings() {
91106 fi
92107}
93108
94- # Reset rules only (do NOT delete NM connection)
95- reset_nic_config () {
109+ find_existing_table_id () {
96110 local nic=" $1 "
97- local table_name=" ${ROUTE_TABLE_PREFIX} -${nic} "
98-
99- echo " 🧹 Resetting routing rules for $nic ..."
100-
101- # Remove matching ip rules
102- local rules
103- rules=$( ip rule show | grep -w " lookup" | grep " $table_name " || true)
104- if [[ -n " $rules " ]]; then
105- echo " $rules " | while read -r rule; do
106- local priority
107- priority=$( echo " $rule " | awk ' {print $1}' | sed ' s/://' )
108- run_or_echo " ip rule del priority $priority "
109- done
110- else
111- echo " No ip rules found for $table_name "
112- fi
113-
114- # Flush the routing table
115- run_or_echo " ip route flush table $table_name || true"
116-
117- echo " ✅ Routing rules cleared for $nic "
111+ grep -E " weka-${nic} " " $RT_TABLES " | awk ' {print $1}' | head -n1
118112}
119113
120- # Get or allocate a routing table ID (ensure unique)
121- get_or_allocate_table_id () {
122- local nic=" $1 "
123- local table_name=" ${ROUTE_TABLE_PREFIX} -${nic} "
124-
125- # Reuse table if it exists
126- local existing_entry
127- existing_entry=$( grep -w " $table_name " " $RT_TABLES " | awk ' {print $1}' || true)
128- if [[ -n " $existing_entry " ]]; then
129- echo " $existing_entry "
130- return
131- fi
114+ find_next_table_id () {
115+ awk ' $1 ~ /^[0-9]+$/ {print $1}' " $RT_TABLES " | sort -n | tail -1 | awk ' {print $1+1}'
116+ }
132117
133- # Allocate next free numeric ID starting from 100
134- local used_ids
135- used_ids=$( awk ' {print $1}' " $RT_TABLES " | grep -E ' ^[0-9]+$' | sort -n | uniq)
136- local id=100
137- while echo " $used_ids " | grep -qw " $id " ; do
138- (( id++ ))
118+ reset_routing_rules () {
119+ for nic in " ${NIC_LIST[@]} " ; do
120+ echo " 🧹 Resetting rules for $nic ..."
121+ local ip_spec
122+ ip_spec=$( get_ip_for_nic " $nic " )
123+ local ip_only=" ${ip_spec%%/* } "
124+ ip rule show | grep -w " $ip_only " | awk -F: ' {print $1}' | while read -r rule_id; do
125+ run_or_echo " ip rule del pref $rule_id "
126+ done
139127 done
140-
141- run_or_echo " echo \" $id $table_name \" >> \" $RT_TABLES \" "
142- echo " $id "
128+ echo " ✅ Reset complete — existing routing rules removed (interfaces untouched)."
129+ exit 0
143130}
144131
145132configure_nic () {
@@ -153,46 +140,39 @@ configure_nic() {
153140 local local_subnet
154141 local_subnet=$( get_network_cidr " $ip_spec " )
155142 local ip_only=" ${ip_spec%%/* } "
156- local con_name=" ${nic} "
157143 local nic_type
158144 nic_type=$( nmcli -g GENERAL.TYPE device show " $nic " 2> /dev/null | head -n1)
159145
160146 echo " ⚙️ Configuring $nic (Type: $nic_type , IP: $ip_only , Local subnet: $local_subnet , Table: $table_name )..."
161147
162- run_or_echo " nmcli con delete \" $con_name \" &>/dev/null || true"
163-
164- run_or_echo " nmcli con add type \" $nic_type \" ifname \" $nic \" con-name \" $con_name \" \
165- ipv4.addresses \" $ip_spec \" \
166- ipv4.method manual \
167- connection.autoconnect yes \
168- ipv4.route-metric 0 \
169- ipv4.routing-rules \" priority $table_id from $ip_only table $table_id \" "
170-
171- if [[ " $nic_type " == " ethernet" ]]; then
172- echo " 📦 Setting MTU 9000 on $nic (Ethernet)"
173- run_or_echo " nmcli con modify \" $con_name \" 802-3-ethernet.mtu 9000"
148+ # Configure routing rule
149+ if ! ip rule show | grep -q " from $ip_only lookup $table_name " ; then
150+ run_or_echo " ip rule add from $ip_only table $table_name pref $table_id "
151+ else
152+ echo " ℹ️ Rule already exists for $ip_only → table $table_name "
174153 fi
175154
176- # Add local subnet route
177- echo " 🔁 Adding route to local subnet: $local_subnet src=$ip_only table=$table_id "
178- run_or_echo " nmcli connection modify \" $con_name \" +ipv4.routes \" $local_subnet src=$ip_only table=$table_id \" "
155+ # Local subnet route
156+ run_or_echo " ip route replace $local_subnet dev $nic src $ip_only table $table_name "
179157
180- # Add client subnet route
158+ # Optional client subnet route (skip main if 0.0.0.0/0)
181159 if [[ -n " $client_subnet " && -n " $gateway " ]]; then
182- echo " 📡 Adding route to client subnet: $client_subnet via $gateway (table: $table_name )"
183- # Always add to NIC's custom table
184- run_or_echo " nmcli connection modify \" $con_name \" +ipv4.routes \" $client_subnet $gateway table=$table_id \" "
185-
186- # Only add to main table if not default route
187- if [[ " $client_subnet " != " 0.0.0.0/0" ]]; then
188- run_or_echo " nmcli connection modify \" $con_name \" +ipv4.routes \" $client_subnet $gateway \" "
160+ if [[ " $client_subnet " == " 0.0.0.0/0" ]]; then
161+ echo " 🌍 Adding default route (client subnet) to table $table_name only"
162+ run_or_echo " ip route replace default via $gateway dev $nic table $table_name "
189163 else
190- echo " ⚠️ Skipping adding 0.0.0.0/0 to main routing table; route exists only in $table_name "
164+ echo " 📡 Adding route to client subnet: $client_subnet via $gateway "
165+ run_or_echo " ip route replace $client_subnet via $gateway dev $nic table $table_name "
191166 fi
192167 fi
193168
194- run_or_echo " nmcli con down \" $con_name \" "
195- run_or_echo " nmcli con up \" $con_name \" "
169+ # Set MTU if Ethernet and not disabled
170+ if [[ " $nic_type " == " ethernet" && $NO_MTU == false ]]; then
171+ echo " 📦 Setting MTU 9000 on $nic (Ethernet)"
172+ run_or_echo " ip link set dev $nic mtu 9000"
173+ elif [[ " $nic_type " == " ethernet" && $NO_MTU == true ]]; then
174+ echo " 🚫 Skipping MTU change on $nic (Ethernet) due to --no-mtu flag"
175+ fi
196176}
197177
198178# ---- MAIN ----
@@ -201,7 +181,6 @@ NIC_LIST=()
201181CLIENT_SUBNET=" "
202182GATEWAY=" "
203183
204- # Parse CLI arguments
205184while [[ $# -gt 0 ]]; do
206185 case " $1 " in
207186 --nics)
@@ -219,12 +198,16 @@ while [[ $# -gt 0 ]]; do
219198 GATEWAY=" $2 "
220199 shift 2
221200 ;;
222- --dry-run )
223- DRY_RUN =true
201+ --reset )
202+ RESET_MODE =true
224203 shift
225204 ;;
226- --reset)
227- RESET=true
205+ --no-mtu)
206+ NO_MTU=true
207+ shift
208+ ;;
209+ --dry-run)
210+ DRY_RUN=true
228211 shift
229212 ;;
230213 * )
@@ -234,27 +217,18 @@ while [[ $# -gt 0 ]]; do
234217 esac
235218done
236219
237- # Validate input
238220if [[ ${# NIC_LIST[@]} -lt 1 ]]; then
239221 errmsg " At least one NIC must be specified"
240222 usage
241223fi
242224
243225check_requirements
226+ apply_sysctl_arp_settings
244227
245- # ---- RESET MODE ----
246- if $RESET ; then
247- echo " 🔄 Reset mode enabled — clearing routing rules and tables for specified NICs."
248- for nic in " ${NIC_LIST[@]} " ; do
249- reset_nic_config " $nic "
250- done
251- echo " ✅ Reset complete. NM connections and ARP settings preserved."
252- exit 0
228+ if $RESET_MODE ; then
229+ reset_routing_rules
253230fi
254231
255- # ---- NORMAL CONFIGURATION ----
256- apply_sysctl_arp_settings
257-
258232for nic in " ${NIC_LIST[@]} " ; do
259233 validate_nic " $nic "
260234 ip_spec=$( get_ip_for_nic " $nic " )
@@ -263,18 +237,22 @@ for nic in "${NIC_LIST[@]}"; do
263237 exit 1
264238 fi
265239
266- table_name=" ${ROUTE_TABLE_PREFIX} -${nic} "
267- table_id=$( get_or_allocate_table_id " $nic " )
240+ table_id=$( find_existing_table_id " $nic " || true)
241+ if [[ -z " $table_id " ]]; then
242+ table_id=$( find_next_table_id)
243+ table_name=" ${ROUTE_TABLE_PREFIX} -${nic} "
244+ ensure_rt_table " $table_id " " $table_name "
245+ else
246+ table_name=$( grep -E " ^[[:space:]]*$table_id " " $RT_TABLES " | awk ' {print $2}' )
247+ fi
268248
269249 configure_nic " $nic " " $ip_spec " " $GATEWAY " " $CLIENT_SUBNET " " $table_name " " $table_id "
270250done
271251
272252if ! $DRY_RUN ; then
273253 echo " ⚙️ Configuring NetworkManager to ignore carrier"
274- echo " [main]" > /etc/NetworkManager/conf.d/99-weka-carrier.conf
275- echo " ignore-carrier=*" >> /etc/NetworkManager/conf.d/99-weka-carrier.conf
254+ echo " [main]" > /etc/NetworkManager/conf.d/99-weka-carrier.conf && echo " ignore-carrier=*" >> /etc/NetworkManager/conf.d/99-weka-carrier.conf
276255fi
277256
278- echo " ✅ ${DRY_RUN:- false} " | grep -q true && tag=' [dry-run] ' || tag=' '
279- echo " ✅ ${tag} Successfully processed ${# NIC_LIST[@]} NIC(s)."
257+ echo " ✅ Successfully processed ${# NIC_LIST[@]} NIC(s)."
280258
0 commit comments