Skip to content

Instantly share code, notes, and snippets.

@Gixs
Last active January 22, 2026 17:55
Show Gist options
  • Select an option

  • Save Gixs/c0a751a6817320c8236501c4dc3c897f to your computer and use it in GitHub Desktop.

Select an option

Save Gixs/c0a751a6817320c8236501c4dc3c897f to your computer and use it in GitHub Desktop.
Mininet-wifi Bonding

🐧 Linux Bonding: Active-Backup

Network Resilience Cheat Sheet

Linux Mode Status


The Goal: One stable interface (bond0) abstracting multiple physical links.
Seamless traffic switching without connection drops.


Topology Overview

graph TD
    User[Traffic / Apps] --> Bond[bond0 <br/> 10.0.0.101]
    Bond -.->|Backup| Link1[sta1-bs1]
    Bond ==>|Active| Link2[sta1-bs2]

Loading

Quick Setup Guide

1. Load the Module

First, ensure the kernel module is loaded.

# Load the bonding driver into the kernel
modprobe bonding

2. Create the Bond

Initialize the logical interface with Active-Backup mode (Mode 1).

# Create bond0 with mode 1 (active-backup) and link monitoring (100ms)
ip link add bond0 type bond mode active-backup miimon=100

# Bring the interface up
ip link set bond0 up

3. Assign IP Address

Crucial: Assign the IP strictly to the bond, never to the slaves.

# Assign the IP to the logical interface
ip addr add 10.0.0.101/8 dev bond0

4. Enslave Physical Interfaces

Attach your physical links (sta1-bsX) to the master bond.

# Set physical interfaces down before bonding
ip link set sta1-bs1 down
ip link set sta1-bs2 down

# Set the master for both interfaces
ip link set sta1-bs1 master bond0
ip link set sta1-bs2 master bond0

# Bring them back up (optional, usually happens automatically)
ip link set sta1-bs1 up
ip link set sta1-bs2 up

Manual Handover (Switching)

Force traffic to switch from one link to another without downtime.

Select Link 1:

# Force sta1-bs1 to become the active slave
echo sta1-bs1 > /sys/class/net/bond0/bonding/active_slave

Select Link 2:

# Force sta1-bs2 to become the active slave
echo sta1-bs2 > /sys/class/net/bond0/bonding/active_slave

Diagnostics

Check the current status, active slave, and link health.

cat /proc/net/bonding/bond0

Golden Rules

Important

To maintain stability, follow these strict rules:

  1. IP Address: Must exist ONLY on bond0.
  2. Applications: Must bind to bond0 (or 0.0.0.0), never to physical links.
  3. Slaves: Physical interfaces (sta1-bsX) must never have an IP configuration.
  4. Control: Handover is controlled via active_slave in sysfs.
Managed by Gixs
#!/usr/bin/python3
"""
This is for test only. Try to refactor it for your project.
"""
from mininet.node import RemoteController, OVSSwitch
from mininet.link import TCLink
from mininet.log import setLogLevel, info
from mn_wifi.net import Mininet_wifi
from mn_wifi.cli import CLI
from ho_cli import HOCLI, install_base_flows_small_topo
import os
def hard_cleanup_intfs(intf_names):
# delete if present (ignore errors)
for n in intf_names:
os.system(f"ip link del {n} >/dev/null 2>&1 || true")
def hard_cleanup_ovs_bridges(bridges):
for b in bridges:
os.system(f"ovs-vsctl --if-exists del-br {b} >/dev/null 2>&1 || true")
def hard_cleanup():
# Mininet cleanup
os.system("mn -c >/dev/null 2>&1 || true")
# OVS cleanup (in case of stale bridges)
hard_cleanup_ovs_bridges(["s1", "bs1", "bs2"])
# Interface names we create explicitly
hard_cleanup_intfs([
"sta1-bs1", "sta1-bs2",
"bs1-access", "bs2-access",
"bs1-uplink", "bs2-uplink",
"s1-bs1", "s1-bs2", "s1-host"
])
def setup_sta_bonding_with_channels(
sta,
bs_list,
channel_list,
*,
ip_cidr="10.0.0.10/24",
bond_name="bond0",
active_bs_name=None, # es: "bs1" (se None, usa il primo in bs_list)
sta_bs_intf_map=None, # dict: { "bs1": "sta1-bs1", "bs2": "sta1-bs2" }
cleanup_bridges=True,
set_default_route=True,
disable_rp_filter=True,
):
"""
Configure a Mininet station as a multi-homed UE using Linux bonding (active-backup)
and apply per-link 'channel' constraints (currently: bandwidth in Mbit/s).
"""
assert len(bs_list) == len(channel_list), "bs_list and channel_list must have the same length"
def sta_intf_for_bs(bs):
if sta_bs_intf_map and bs.name in sta_bs_intf_map:
return sta_bs_intf_map[bs.name]
# Default convention (ONLY if you used these names in addLink):
return f"{sta.name}-{bs.name}"
def parse_bw_mbit(ch):
if isinstance(ch, (int, float)):
return float(ch)
if isinstance(ch, dict):
bw = ch.get("bw_mbit", None)
if bw is None:
raise ValueError("Channel dict must include 'bw_mbit'")
return float(bw)
raise ValueError(f"Unsupported channel type: {type(ch)}")
def tc_set_rate(dev, bw_mbit):
"""
Apply shaping to a device using TBF (robust across environments).
bw_mbit in Mbit/s.
"""
sta.cmd(f"tc qdisc del dev {dev} root 2>/dev/null || true")
sta.cmd(
f"tc qdisc add dev {dev} root tbf "
f"rate {bw_mbit}mbit burst 256kbit latency 50ms"
)
# ---------- Cleanup leftovers that can break routing choices ----------
sta.cmd("ip link set lo up")
if cleanup_bridges:
sta.cmd("ip route del 10.0.0.0/24 dev br0 2>/dev/null || true")
sta.cmd("ip link set br0 down 2>/dev/null || true")
sta.cmd("ip link del br0 2>/dev/null || true")
# Remove previous bond if exists
sta.cmd(f"ip route del default dev {bond_name} metric 100 2>/dev/null || true")
sta.cmd(f"ip route del default dev {bond_name} 2>/dev/null || true")
sta.cmd(f"ip addr flush dev {bond_name} 2>/dev/null || true")
sta.cmd(f"ip link del {bond_name} 2>/dev/null || true")
# Flush IP from the sta<->BS interfaces (avoid ambiguity)
for bs in bs_list:
sif = sta_intf_for_bs(bs)
sta.cmd(f"ip addr flush dev {sif} 2>/dev/null || true")
# ---------- Create bond ----------
sta.cmd("modprobe bonding")
sta.cmd(f"ip link add {bond_name} type bond mode active-backup miimon 100")
# Enslave all sta<->BS interfaces
sta_ifaces = []
for bs in bs_list:
sif = sta_intf_for_bs(bs)
sta_ifaces.append(sif)
sta.cmd(f"ip link set {sif} down 2>/dev/null || true")
sta.cmd(f"ip link set {sif} master {bond_name}")
# Bring up bond + slaves
sta.cmd(f"ip link set {bond_name} up")
for sif in sta_ifaces:
sta.cmd(f"ip link set {sif} up")
# Assign IP only on bond
sta.cmd(f"ip addr add {ip_cidr} dev {bond_name} 2>/dev/null || true")
if set_default_route:
sta.cmd(f"ip route del default dev {bond_name} 2>/dev/null || true")
sta.cmd(f"ip route add default dev {bond_name} metric 100 2>/dev/null || true")
if disable_rp_filter:
sta.cmd("sysctl -w net.ipv4.conf.all.rp_filter=0 >/dev/null")
sta.cmd("sysctl -w net.ipv4.conf.default.rp_filter=0 >/dev/null")
sta.cmd(f"sysctl -w net.ipv4.conf.{bond_name}.rp_filter=0 >/dev/null")
# ---------- Choose initial active BS ----------
if active_bs_name is None:
active_bs = bs_list[0]
else:
active_bs = next((b for b in bs_list if b.name == active_bs_name), bs_list[0])
active_if = sta_intf_for_bs(active_bs)
sta.cmd(f"sh -c 'echo {active_if} > /sys/class/net/{bond_name}/bonding/active_slave'")
# ---------- Apply channel bandwidth per link ----------
# --- Keep bond MAC stable across handover ---
# Do NOT change MAC on slave switch
sta.cmd(f"sh -c 'echo 0 > /sys/class/net/{bond_name}/bonding/fail_over_mac'")
# Force bond0 to use a fixed MAC (take it from the initial active interface)
sta.cmd(
f"sh -c 'MAC=$(cat /sys/class/net/{active_if}/address); "
f"ip link set dev {bond_name} address $MAC'"
)
applied = []
for bs, ch in zip(bs_list, channel_list):
sif = sta_intf_for_bs(bs)
bw = parse_bw_mbit(ch)
tc_set_rate(sif, bw)
applied.append((bs.name, sif, bw))
return {
"bond": bond_name,
"active_bs": active_bs.name,
"active_if": active_if,
"applied_channels": applied,
}
def topology():
#Hard cleanup
hard_cleanup()
# Ensure bonding module is loaded at host level (best effort)
os.system("modprobe bonding >/dev/null 2>&1")
net = Mininet_wifi(
controller=RemoteController,
switch=OVSSwitch,
link=TCLink,
build=False,
autoAssociation=False
)
info("*** Controller (ONOS)\n")
c0 = net.addController(
name="c0",
controller=RemoteController,
ip="127.0.0.1",
port=6653
)
info("*** Switches (fixed DPIDs)\n")
s1 = net.addSwitch("s1", protocols="OpenFlow13", dpid="0000000000000001")
bs1 = net.addSwitch("bs1", protocols="OpenFlow13", dpid="0000000000000011")
bs2 = net.addSwitch("bs2", protocols="OpenFlow13", dpid="0000000000000012")
info("*** Station + Host\n")
sta1 = net.addStation("sta1")
h1 = net.addHost("h1", ip="10.0.0.1/24")
info("*** Wired links (5G-like dual-homing)\n")
# Access links (station <-> BS) with clear interface names
net.addLink(sta1, bs1, intfName1="sta1-bs1", intfName2="bs1-access")
net.addLink(sta1, bs2, intfName1="sta1-bs2", intfName2="bs2-access")
# Uplinks BS -> core (clear names on both sides)
net.addLink(bs1, s1)
net.addLink(bs2, s1)
# Test host behind core
net.addLink(h1, s1)
info("*** Build + start\n")
net.build()
c0.start()
for sw in (s1, bs1, bs2):
sw.start([c0])
# Configure bonding (your existing function)
setup_sta_bonding_with_channels(
sta=sta1,
bs_list=[bs1, bs2],
channel_list=[1000, 200],
ip_cidr="10.0.0.10/24",
active_bs_name="bs1"
)
# Install base flows deterministically (serving bs1)
install_base_flows_small_topo(serving_bs="bs1")
# Prime ARP/neigh once at boot (optional but recommended)
sta1.cmd("ip neigh flush all || true")
h1.cmd("ip neigh flush all || true")
sta1.cmd("ping -c 1 -I bond0 10.0.0.1 >/dev/null 2>&1 || true")
HOCLI(net)
net.stop()
info("""
------------------------------------------------
Topology:
bs1 ----
/ \\
sta1 --(wired) s1 ---- h1(10.0.0.1/24)
\\ /
bs2 ----
sta1:
- sta1-bs1 (slave) -> bs1-access
- sta1-bs2 (slave) -> bs2-access
- bond0 (active-backup) -> 10.0.0.10/24
ONOS controller: 127.0.0.1:6653
DPIDs:
- s1 = of:0000000000000001
- bs1 = of:0000000000000011
- bs2 = of:0000000000000012
Channel shaping (station side):
- sta1-bs1: 1000 Mbit/s
- sta1-bs2: 200 Mbit/s
------------------------------------------------
""")
if __name__ == "__main__":
setLogLevel("info")
topology()
"""
This is for test only. Try to refactor it for your project.
"""
# lib/ho_cli.py
# ---------------------------------------------
# Clean ONOS flow management + Mininet-WiFi CLI commands:
# - ho bs1|bs2 : make-before-break handover (delete old HO flows, install new)
# - resetflows [bs1] : delete ALL our flows (s1/bs1/bs2) and reinstall base flows
# - hoshow : show current active slave and current serving target
#
# Notes:
# - We tag ALL flows we install with a fixed cookie (MY_COOKIE). This lets us delete ONLY our flows
# without touching ONOS system flows (LLDP/BDDP, discovery, etc.).
# - We use delete-then-add for the HO flow, so every new flow replaces the previous one cleanly.
# - Port mapping for your small topology (confirmed by `net`):
# s1-eth1 <-> bs1-eth2 => P_S1_BS1 = 1
# s1-eth2 <-> bs2-eth2 => P_S1_BS2 = 2
# s1-eth3 <-> h1-eth0 => P_S1_HOST = 3
# bs1: access=1, uplink=2 ; bs2: access=1, uplink=2
#
# Requirements:
# - ONOS REST enabled on http://127.0.0.1:8181 (default credentials onos/rocks)
import json
import base64
import time
import urllib.request
import urllib.error
from mn_wifi.cli import CLI as MNWIFI_CLI
# ---------------- ONOS REST CONFIG ----------------
ONOS_USER = "onos"
ONOS_PASS = "rocks"
ONOS_BASE = "http://127.0.0.1:8181/onos/v1"
# ---------------- TOPO CONSTANTS (SMALL TOPO) ----------------
S1 = "of:0000000000000001"
BS1 = "of:0000000000000011"
BS2 = "of:0000000000000012"
P_S1_BS1 = "1"
P_S1_BS2 = "2"
P_S1_HOST = "3"
UE_IP = "10.0.0.10/32" # UE on bond0
H1_IP = "10.0.0.1" # test host
# Tag all our flows with this cookie so we can delete them cleanly
MY_COOKIE = "0x5a5a5a5a"
# ---------------- REST HELPERS ----------------
def _auth_header():
token = base64.b64encode(f"{ONOS_USER}:{ONOS_PASS}".encode()).decode()
return {"Authorization": f"Basic {token}"}
def onos_get(path: str, timeout_s: int = 5):
req = urllib.request.Request(ONOS_BASE + path, method="GET")
req.add_header("Accept", "application/json")
for k, v in _auth_header().items():
req.add_header(k, v)
with urllib.request.urlopen(req, timeout=timeout_s) as r:
return json.loads(r.read().decode("utf-8", errors="ignore"))
def onos_post(path: str, payload: dict, timeout_s: int = 5):
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(ONOS_BASE + path, data=data, method="POST")
req.add_header("Content-Type", "application/json")
for k, v in _auth_header().items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req, timeout=timeout_s) as r:
body = r.read().decode("utf-8", errors="ignore")
return r.status, body
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="ignore")
return e.code, body
except Exception as e:
return -1, str(e)
def onos_delete(path: str, timeout_s: int = 5):
req = urllib.request.Request(ONOS_BASE + path, method="DELETE")
for k, v in _auth_header().items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req, timeout=timeout_s) as r:
body = r.read().decode("utf-8", errors="ignore")
return r.status, body
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="ignore")
return e.code, body
except Exception as e:
return -1, str(e)
def wait_onos_device(device_id: str, timeout_s: int = 20, poll_s: float = 0.5) -> bool:
"""Wait until ONOS reports a device as available."""
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
data = onos_get(f"/devices/{device_id}")
if bool(data.get("available", False)):
return True
except Exception:
pass
time.sleep(poll_s)
return False
def _cookie_to_hex(cookie_val):
"""Normalize ONOS cookie (could be str or int) to lower-case hex string like '0x1234'."""
if cookie_val is None:
return None
if isinstance(cookie_val, str):
s = cookie_val.strip().lower()
# ensure it starts with 0x for comparison
if s.startswith("0x"):
return s
try:
return hex(int(s))
except Exception:
return s
try:
return hex(int(cookie_val)).lower()
except Exception:
return None
def delete_flows_by_cookie(device_id: str, cookie_hex: str) -> int:
"""Delete ONLY flows on device_id that have cookie == cookie_hex."""
data = onos_get(f"/flows/{device_id}")
flows = data.get("flows", [])
deleted = 0
for f in flows:
c_hex = _cookie_to_hex(f.get("cookie"))
if c_hex and c_hex == cookie_hex.lower():
flow_id = f.get("id")
if flow_id:
st, _ = onos_delete(f"/flows/{device_id}/{flow_id}")
if st in (200, 204):
deleted += 1
return deleted
def add_flow(device_id: str, priority: int, criteria: list, instructions: list, *, cookie_hex: str, permanent: bool = True):
payload = {
"deviceId": device_id,
"cookie": cookie_hex,
"priority": int(priority),
"isPermanent": bool(permanent),
"selector": {"criteria": criteria},
"treatment": {"instructions": instructions},
}
return onos_post(f"/flows/{device_id}", payload)
# ---------------- FLOW INSTALLERS (BASE + HO) ----------------
def install_base_flows_small_topo(serving_bs: str = "bs1", *, base_prio: int = 50000):
"""
Install deterministic base flows for the small topology:
- BS1/BS2: bridge ARP+IPv4 between access(1) and uplink(2)
- S1: uplink ARP+IPv4 from BS ports to host port
- S1: downlink ARP host->serving, IPv4 host->UE->serving (IP-based anchor)
"""
serving_bs = serving_bs.lower().strip()
if serving_bs not in ("bs1", "bs2"):
serving_bs = "bs1"
# Wait devices (prevents "first rule not initialized" race)
for dev in (S1, BS1, BS2):
if not wait_onos_device(dev, timeout_s=25):
raise RuntimeError(f"ONOS device not available: {dev}")
out_port = P_S1_BS1 if serving_bs == "bs1" else P_S1_BS2
# ---- BS bridge flows (higher prio, ETH_TYPE specific) ----
for dev in (BS1, BS2):
# access(1)->uplink(2)
add_flow(dev, base_prio,
[{"type": "IN_PORT", "port": "1"}, {"type": "ETH_TYPE", "ethType": "0x0806"}],
[{"type": "OUTPUT", "port": "2"}],
cookie_hex=MY_COOKIE)
add_flow(dev, base_prio,
[{"type": "IN_PORT", "port": "1"}, {"type": "ETH_TYPE", "ethType": "0x0800"}],
[{"type": "OUTPUT", "port": "2"}],
cookie_hex=MY_COOKIE)
# uplink(2)->access(1)
add_flow(dev, base_prio,
[{"type": "IN_PORT", "port": "2"}, {"type": "ETH_TYPE", "ethType": "0x0806"}],
[{"type": "OUTPUT", "port": "1"}],
cookie_hex=MY_COOKIE)
add_flow(dev, base_prio,
[{"type": "IN_PORT", "port": "2"}, {"type": "ETH_TYPE", "ethType": "0x0800"}],
[{"type": "OUTPUT", "port": "1"}],
cookie_hex=MY_COOKIE)
# ---- S1 uplink (BS->HOST) ----
add_flow(S1, base_prio,
[{"type": "IN_PORT", "port": P_S1_BS1}, {"type": "ETH_TYPE", "ethType": "0x0806"}],
[{"type": "OUTPUT", "port": P_S1_HOST}],
cookie_hex=MY_COOKIE)
add_flow(S1, base_prio,
[{"type": "IN_PORT", "port": P_S1_BS2}, {"type": "ETH_TYPE", "ethType": "0x0806"}],
[{"type": "OUTPUT", "port": P_S1_HOST}],
cookie_hex=MY_COOKIE)
add_flow(S1, base_prio,
[{"type": "IN_PORT", "port": P_S1_BS1}, {"type": "ETH_TYPE", "ethType": "0x0800"}],
[{"type": "OUTPUT", "port": P_S1_HOST}],
cookie_hex=MY_COOKIE)
add_flow(S1, base_prio,
[{"type": "IN_PORT", "port": P_S1_BS2}, {"type": "ETH_TYPE", "ethType": "0x0800"}],
[{"type": "OUTPUT", "port": P_S1_HOST}],
cookie_hex=MY_COOKIE)
# ---- S1 downlink ARP host->serving ----
add_flow(S1, base_prio + 1000,
[{"type": "IN_PORT", "port": P_S1_HOST}, {"type": "ETH_TYPE", "ethType": "0x0806"}],
[{"type": "OUTPUT", "port": out_port}],
cookie_hex=MY_COOKIE)
# ---- S1 downlink IPv4 host->UE->serving (anchor rule) ----
add_flow(S1, base_prio + 2000,
[{"type": "IN_PORT", "port": P_S1_HOST},
{"type": "ETH_TYPE", "ethType": "0x0800"},
{"type": "IPV4_DST", "ip": UE_IP}],
[{"type": "OUTPUT", "port": out_port}],
cookie_hex=MY_COOKIE)
return {"base_serving": serving_bs, "cookie": MY_COOKIE}
def install_ho_rules_s1(target_bs: str, *, ho_prio: int):
"""Install only the HO-specific rules (downlink IPv4 + ARP host->serving) on S1."""
target_bs = target_bs.lower().strip()
out_port = P_S1_BS1 if target_bs == "bs1" else P_S1_BS2
# Remove previous HO rules (but keep base flows)
# We separate HO cookie from base cookie to delete just HO rules.
HO_COOKIE = "0x5a5a5a5b" # different cookie for HO rules
delete_flows_by_cookie(S1, HO_COOKIE)
# HO ARP host->serving (slightly higher than base)
st1, body1 = add_flow(S1, ho_prio + 1,
[{"type": "IN_PORT", "port": P_S1_HOST}, {"type": "ETH_TYPE", "ethType": "0x0806"}],
[{"type": "OUTPUT", "port": out_port}],
cookie_hex=HO_COOKIE)
# HO IPv4 downlink host->UE->serving
st2, body2 = add_flow(S1, ho_prio,
[{"type": "IN_PORT", "port": P_S1_HOST},
{"type": "ETH_TYPE", "ethType": "0x0800"},
{"type": "IPV4_DST", "ip": UE_IP}],
[{"type": "OUTPUT", "port": out_port}],
cookie_hex=HO_COOKIE)
return (st1, body1, st2, body2)
# ---------------- CLI ----------------
class HOCLI(MNWIFI_CLI):
"""
Commands:
- ho bs1|bs2
- resetflows [bs1|bs2]
- hoshow
"""
def __init__(self, *args, **kwargs):
# Mininet CLI enters cmdloop inside super().__init__(), so initialize state first.
self.ho_prio = 65000
self.current_serving = "bs1"
super().__init__(*args, **kwargs)
def do_hoshow(self, _line):
sta = self.mn.get("sta1")
active = sta.cmd("cat /sys/class/net/bond0/bonding/active_slave 2>/dev/null || true").strip()
print(f"serving={self.current_serving}, ho_prio={self.ho_prio}, active_slave={active}")
def do_resetflows(self, line):
"""
resetflows [bs1|bs2]
- Deletes ONLY our flows (cookie-tagged) on s1/bs1/bs2, reinstalls base flows.
- Optionally sets initial serving BS (default bs1).
"""
args = line.split()
serving = args[0] if (len(args) == 1 and args[0] in ("bs1", "bs2")) else "bs1"
# Delete all base flows (MY_COOKIE) from all devices
try:
d1 = delete_flows_by_cookie(S1, MY_COOKIE)
d2 = delete_flows_by_cookie(BS1, MY_COOKIE)
d3 = delete_flows_by_cookie(BS2, MY_COOKIE)
# Also delete HO cookie rules if present
delete_flows_by_cookie(S1, "0x5a5a5a5b")
info = install_base_flows_small_topo(serving_bs=serving)
# Prime ARP/neighbor after reset
sta = self.mn.get("sta1")
h1 = self.mn.get("h1")
sta.cmd("ip neigh flush all || true")
h1.cmd("ip neigh flush all || true")
sta.cmd(f"ping -c 1 -I bond0 {H1_IP} >/dev/null 2>&1 || true")
# Keep internal state aligned
self.current_serving = serving
print(f"resetflows: deleted s1={d1}, bs1={d2}, bs2={d3}, base_serving={info['base_serving']}")
except Exception as e:
print(f"resetflows error: {e}")
def do_ho(self, line):
"""
ho bs1|bs2
Make-before-break HO:
1) Update S1 downlink for UE (delete previous HO flows, install new)
2) Switch bonding active slave on sta1
3) Flush neigh + prime ping to avoid TCP stalling
"""
args = line.split()
if len(args) != 1 or args[0] not in ("bs1", "bs2"):
print("Usage: ho bs1|bs2")
return
target = args[0]
sta = self.mn.get("sta1")
# Ensure monotonic priority
self.ho_prio += 10
# 1) Core update (delete-then-add HO rules on S1)
st1, body1, st2, body2 = install_ho_rules_s1(target, ho_prio=self.ho_prio)
if st1 not in (200, 201) or st2 not in (200, 201):
print(f"ONOS HO error: arp_status={st1}, ipv4_status={st2}")
if body1:
print(f"arp_body: {body1}")
if body2:
print(f"ipv4_body: {body2}")
return
# 2) UE switch
target_if = "sta1-bs1" if target == "bs1" else "sta1-bs2"
sta.cmd(f"sh -c 'echo {target_if} > /sys/class/net/bond0/bonding/active_slave'")
# 3) Flush neighbor + prime
sta.cmd("ip neigh flush all || true")
sta.cmd(f"ping -c 1 -I bond0 {H1_IP} >/dev/null 2>&1 || true")
self.current_serving = target
active = sta.cmd("cat /sys/class/net/bond0/bonding/active_slave 2>/dev/null || true").strip()
print(f"HO -> {target} (prio={self.ho_prio}, active_slave={active})")
"""
I use it inside the topology to map to the differetn basestation.
You can modify it and import in the same file of the topology.
"""
def setup_sta_bonding_with_channels(
sta,
bs_list,
channel_list,
*,
ip_cidr="10.0.0.10/24",
bond_name="bond0",
active_bs_name=None, # es: "bs1" (se None, usa il primo in bs_list)
sta_bs_intf_map=None, # dict: { "bs1": "sta1-bs1", "bs2": "sta1-bs2" }
cleanup_bridges=True,
set_default_route=True,
disable_rp_filter=True,
):
"""
Configure a Mininet station as a multi-homed UE using Linux bonding (active-backup)
and apply per-link 'channel' constraints (currently: bandwidth in Mbit/s).
"""
assert len(bs_list) == len(channel_list), "bs_list and channel_list must have the same length"
def sta_intf_for_bs(bs):
if sta_bs_intf_map and bs.name in sta_bs_intf_map:
return sta_bs_intf_map[bs.name]
# Default convention (ONLY if you used these names in addLink):
return f"{sta.name}-{bs.name}"
def parse_bw_mbit(ch):
if isinstance(ch, (int, float)):
return float(ch)
if isinstance(ch, dict):
bw = ch.get("bw_mbit", None)
if bw is None:
raise ValueError("Channel dict must include 'bw_mbit'")
return float(bw)
raise ValueError(f"Unsupported channel type: {type(ch)}")
def tc_set_rate(dev, bw_mbit):
"""
Apply shaping to a device using TBF (robust across environments).
bw_mbit in Mbit/s.
"""
sta.cmd(f"tc qdisc del dev {dev} root 2>/dev/null || true")
sta.cmd(
f"tc qdisc add dev {dev} root tbf "
f"rate {bw_mbit}mbit burst 256kbit latency 50ms"
)
# ---------- Cleanup leftovers that can break routing choices ----------
sta.cmd("ip link set lo up")
if cleanup_bridges:
sta.cmd("ip route del 10.0.0.0/24 dev br0 2>/dev/null || true")
sta.cmd("ip link set br0 down 2>/dev/null || true")
sta.cmd("ip link del br0 2>/dev/null || true")
# Remove previous bond if exists
sta.cmd(f"ip route del default dev {bond_name} metric 100 2>/dev/null || true")
sta.cmd(f"ip route del default dev {bond_name} 2>/dev/null || true")
sta.cmd(f"ip addr flush dev {bond_name} 2>/dev/null || true")
sta.cmd(f"ip link del {bond_name} 2>/dev/null || true")
# Flush IP from the sta<->BS interfaces (avoid ambiguity)
for bs in bs_list:
sif = sta_intf_for_bs(bs)
sta.cmd(f"ip addr flush dev {sif} 2>/dev/null || true")
# ---------- Create bond ----------
sta.cmd("modprobe bonding")
sta.cmd(f"ip link add {bond_name} type bond mode active-backup miimon 100")
# Enslave all sta<->BS interfaces
sta_ifaces = []
for bs in bs_list:
sif = sta_intf_for_bs(bs)
sta_ifaces.append(sif)
sta.cmd(f"ip link set {sif} down 2>/dev/null || true")
sta.cmd(f"ip link set {sif} master {bond_name}")
# Bring up bond + slaves
sta.cmd(f"ip link set {bond_name} up")
for sif in sta_ifaces:
sta.cmd(f"ip link set {sif} up")
# Assign IP only on bond
sta.cmd(f"ip addr add {ip_cidr} dev {bond_name} 2>/dev/null || true")
if set_default_route:
sta.cmd(f"ip route del default dev {bond_name} 2>/dev/null || true")
sta.cmd(f"ip route add default dev {bond_name} metric 100 2>/dev/null || true")
if disable_rp_filter:
sta.cmd("sysctl -w net.ipv4.conf.all.rp_filter=0 >/dev/null")
sta.cmd("sysctl -w net.ipv4.conf.default.rp_filter=0 >/dev/null")
sta.cmd(f"sysctl -w net.ipv4.conf.{bond_name}.rp_filter=0 >/dev/null")
# ---------- Choose initial active BS ----------
if active_bs_name is None:
active_bs = bs_list[0]
else:
active_bs = next((b for b in bs_list if b.name == active_bs_name), bs_list[0])
active_if = sta_intf_for_bs(active_bs)
sta.cmd(f"sh -c 'echo {active_if} > /sys/class/net/{bond_name}/bonding/active_slave'")
# ---------- Apply channel bandwidth per link ----------
# --- Keep bond MAC stable across handover ---
# Do NOT change MAC on slave switch
sta.cmd(f"sh -c 'echo 0 > /sys/class/net/{bond_name}/bonding/fail_over_mac'")
# Force bond0 to use a fixed MAC (take it from the initial active interface)
sta.cmd(
f"sh -c 'MAC=$(cat /sys/class/net/{active_if}/address); "
f"ip link set dev {bond_name} address $MAC'"
)
applied = []
for bs, ch in zip(bs_list, channel_list):
sif = sta_intf_for_bs(bs)
bw = parse_bw_mbit(ch)
tc_set_rate(sif, bw)
applied.append((bs.name, sif, bw))
return {
"bond": bond_name,
"active_bs": active_bs.name,
"active_if": active_if,
"applied_channels": applied,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment