Posted on
Artificial Intelligence

Artificial Intelligence SDN on Linux

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Artificial Intelligence SDN on Linux: Build an Adaptive Network Brain with Bash and Open vSwitch

If your network still runs on static ACLs and manual change windows, you’re playing whack‑a‑mole in a world of shape‑shifting traffic. Software‑Defined Networking (SDN) gives you centralized control; Artificial Intelligence (AI) gives you adaptive insight. On Linux, you can combine both—today—to create a small but powerful “self-driving” network lab that observes, learns, and reacts in real time.

This article explains why AI + SDN on Linux is worth your time, and walks you through a hands-on mini-lab using Open vSwitch (OVS) and a Python-based SDN controller (Ryu) that uses a simple ML model to detect bursts and auto-block suspicious flows. All commands are Linux-friendly and shown for apt, dnf, and zypper where relevant.

Why AI + SDN on Linux is a good idea

  • Centralized control meets probabilistic intelligence. SDN lets you change the network with API calls. AI helps you decide what to change, and when.

  • Linux gives you the toolbox. Open vSwitch, Python, and controllers like Ryu run natively. You can prototype on a laptop and scale up to servers.

  • Real value, right now:

    • DDoS/DoH anomaly detection from flow/port telemetry
    • Auto‑QoS and path selection based on traffic patterns
    • Microsegmentation that adapts to workload behavior
    • Closed-loop automation (observe → decide → act)

What follows is a minimal “closed loop” you can build in under an hour.


Architecture overview (what you’ll build)

  • Data plane: Open vSwitch bridge on Linux

  • Test hosts: Two Linux network namespaces (h1, h2)

  • Controller: Ryu (OpenFlow 1.3) running a Python app

  • AI model: A small scikit-learn IsolationForest for anomaly detection

  • Loop: OVS flows → Ryu stats → AI decision → OVS actions (allow/drop)


1) Install prerequisites

You’ll need root privileges for networking commands (sudo).

Packages:

  • Open vSwitch

  • Python 3 + pip

  • iproute2 (for network namespaces and veth)

  • Optional: git, tmux

Debian/Ubuntu (apt):

sudo apt update
sudo apt install -y openvswitch-switch python3 python3-pip python3-venv iproute2 git
sudo systemctl enable --now openvswitch-switch

Fedora/RHEL/CentOS/Rocky (dnf):

sudo dnf install -y openvswitch python3 python3-pip iproute git
sudo systemctl enable --now openvswitch

openSUSE/SLE (zypper):

sudo zypper refresh
sudo zypper install -y openvswitch-switch python3 python3-pip iproute2 git
sudo systemctl enable --now openvswitch

Create a Python virtual environment (recommended) and install dependencies:

python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install ryu scikit-learn joblib numpy

Note: If you prefer not to use venv, you can install user-wide with:

python3 -m pip install --user ryu scikit-learn joblib numpy

Make sure your PATH includes ~/.local/bin if you go the user route.


2) Build a tiny SDN playground with Linux namespaces + OVS

Create two hosts (h1, h2), connect them to an OVS bridge (br0), and point OVS at our controller.

# Create namespaces
sudo ip netns add h1
sudo ip netns add h2

# Create an OVS bridge
sudo ovs-vsctl --may-exist add-br br0

# Create veth pairs: (h1 <-> br0) and (h2 <-> br0)
sudo ip link add veth-h1 type veth peer name veth-br1
sudo ip link add veth-h2 type veth peer name veth-br2

# Move one end into each namespace
sudo ip link set veth-h1 netns h1
sudo ip link set veth-h2 netns h2

# Attach the other ends to OVS
sudo ovs-vsctl --may-exist add-port br0 veth-br1
sudo ovs-vsctl --may-exist add-port br0 veth-br2

# Bring up the OVS bridge and OVS-facing veth ports
sudo ip link set br0 up
sudo ip link set veth-br1 up
sudo ip link set veth-br2 up

# Assign IPs and bring up interfaces and loopbacks in namespaces
sudo ip netns exec h1 ip addr add 10.0.0.1/24 dev veth-h1
sudo ip netns exec h1 ip link set veth-h1 up
sudo ip netns exec h1 ip link set lo up

sudo ip netns exec h2 ip addr add 10.0.0.2/24 dev veth-h2
sudo ip netns exec h2 ip link set veth-h2 up
sudo ip netns exec h2 ip link set lo up

# Point OVS to a local controller on TCP/6653 and use 'secure' fail mode
sudo ovs-vsctl set-controller br0 tcp:127.0.0.1:6653
sudo ovs-vsctl set-fail-mode br0 secure

Sanity check (no controller yet, so this may hang—run again after the controller starts):

sudo ip netns exec h1 ping -c 1 10.0.0.2

3) Train a tiny anomaly detector

We’ll train a simple IsolationForest on synthetic “normal” flow rates (packets/sec and bytes/sec). In production, train on your own baseline telemetry.

Create train_model.py:

cat > train_model.py << 'EOF'
import numpy as np
from sklearn.ensemble import IsolationForest
from joblib import dump
rng = np.random.default_rng(42)

# Synthetic "normal" traffic: pkts/sec ~ N(60, 15), bytes/sec ~ N(8000, 2000)
pkts = rng.normal(loc=60, scale=15, size=2000).clip(min=1)
bytes_ = rng.normal(loc=8000, scale=2000, size=2000).clip(min=200)
X = np.column_stack([pkts, bytes_])

model = IsolationForest(
    n_estimators=200,
    contamination=0.05,
    random_state=42
).fit(X)

dump(model, "model.joblib")
print("Wrote model.joblib")
EOF

Run it:

python3 train_model.py

This produces model.joblib that classifies “normal-like” rates as 1 and anomalies as -1.


4) Run a Ryu controller that learns and reacts

The app below:

  • Installs a table-miss rule to forward unknown packets to the controller.

  • On first packet of a new flow, installs a short-lived flow that uses NORMAL forwarding so traffic flows at line rate.

  • Periodically queries flow stats; computes simple (pkts/sec, bytes/sec) deltas; classifies with the ML model.

  • If a flow looks anomalous, it installs a high-priority drop rule for that flow’s source/destination.

Create ai_guard.py:

cat > ai_guard.py << 'EOF'
from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER, DEAD_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet, ethernet, ipv4, tcp, udp
from ryu.lib import hub
from joblib import load
import time

def flow_key_from_pkt(msg):
    pkt = packet.Packet(msg.data)
    eth = pkt.get_protocol(ethernet.ethernet)
    ip4 = pkt.get_protocol(ipv4.ipv4)
    if not ip4:
        return None
    l4 = pkt.get_protocol(tcp.tcp) or pkt.get_protocol(udp.udp)
    proto = ip4.proto
    sport = getattr(l4, 'src_port', 0)
    dport = getattr(l4, 'dst_port', 0)
    return (ip4.src, ip4.dst, proto, sport, dport)

class AIGuard(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        super(AIGuard, self).__init__(*args, **kwargs)
        self.datapath = None
        self.model = load("model.joblib")
        self.prev_counts = {}  # flow_key -> (prev_pkts, prev_bytes, prev_time)
        self.monitor_thread = hub.spawn(self._monitor)

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        dp = ev.msg.datapath
        self.datapath = dp
        ofp = dp.ofproto
        parser = dp.ofproto_parser

        # Table-miss: send to controller
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofp.OFPP_CONTROLLER, ofp.OFPCML_NO_BUFFER)]
        self.add_flow(dp, priority=0, match=match, actions=actions)
        self.logger.info("Installed table-miss rule")

    def add_flow(self, dp, priority, match, actions=None, buffer_id=None, idle_timeout=0, hard_timeout=0):
        ofp = dp.ofproto
        parser = dp.ofproto_parser
        inst = []
        if actions is not None:
            inst = [parser.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(datapath=dp, priority=priority,
                                match=match, instructions=inst,
                                buffer_id=buffer_id if buffer_id is not None else ofp.OFP_NO_BUFFER,
                                idle_timeout=idle_timeout, hard_timeout=hard_timeout)
        dp.send_msg(mod)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def packet_in_handler(self, ev):
        msg = ev.msg
        dp = msg.datapath
        ofp = dp.ofproto
        parser = dp.ofproto_parser

        key = flow_key_from_pkt(msg)
        if not key:
            # Non-IPv4: just let NORMAL handle it
            actions = [parser.OFPActionOutput(ofp.OFPP_NORMAL)]
            out = parser.OFPPacketOut(datapath=dp, buffer_id=ofp.OFP_NO_BUFFER,
                                      in_port=msg.match['in_port'], actions=actions, data=msg.data)
            dp.send_msg(out)
            return

        src, dst, proto, sport, dport = key

        # Install a short-lived NORMAL forwarding rule for this 5-tuple
        match_fields = dict(eth_type=0x0800, ipv4_src=src, ipv4_dst=dst, ip_proto=proto)
        if proto == 6:  # TCP
            match_fields.update(tcp_src=sport, tcp_dst=dport)
        elif proto == 17:  # UDP
            match_fields.update(udp_src=sport, udp_dst=dport)

        match = parser.OFPMatch(**match_fields)
        actions = [parser.OFPActionOutput(ofp.OFPP_NORMAL)]
        self.add_flow(dp, priority=10, match=match, actions=actions, idle_timeout=30)

        # Also forward this initial packet
        out = parser.OFPPacketOut(datapath=dp, buffer_id=ofp.OFP_NO_BUFFER,
                                  in_port=msg.match['in_port'], actions=actions, data=msg.data)
        dp.send_msg(out)

    def _monitor(self):
        while True:
            if self.datapath and self.datapath.state != DEAD_DISPATCHER:
                self._request_flow_stats(self.datapath)
            hub.sleep(2)

    def _request_flow_stats(self, dp):
        parser = dp.ofproto_parser
        req = parser.OFPFlowStatsRequest(dp)
        dp.send_msg(req)

    @set_ev_cls(ofp_event.EventOFPFlowStatsReply, MAIN_DISPATCHER)
    def flow_stats_reply_handler(self, ev):
        now = time.time()
        for stat in ev.msg.body:
            # Only look at our priority-10 flows with eth_type=IPv4
            if stat.priority != 10:
                continue
            match = stat.match
            if match.get('eth_type') != 0x0800:
                continue

            key = (match.get('ipv4_src'),
                   match.get('ipv4_dst'),
                   match.get('ip_proto'),
                   match.get('tcp_src', match.get('udp_src', 0)),
                   match.get('tcp_dst', match.get('udp_dst', 0)))

            prev = self.prev_counts.get(key)
            pkts = stat.packet_count
            bytes_ = stat.byte_count
            if prev:
                prev_pkts, prev_bytes, prev_t = prev
                dt = max(0.001, now - prev_t)
                pkts_per_s = (pkts - prev_pkts) / dt
                bytes_per_s = (bytes_ - prev_bytes) / dt

                # Classify using the ML model
                X = [[pkts_per_s, bytes_per_s]]
                pred = self.model.predict(X)[0]  # 1 = normal, -1 = anomaly

                if pred == -1:
                    self.logger.warning("Anomalous flow detected %s: %.1f pps, %.1f Bps -> dropping", key, pkts_per_s, bytes_per_s)
                    self._drop_flow(ev.msg.datapath, match)
            # Update counters
            self.prev_counts[key] = (pkts, bytes_, now)

    def _drop_flow(self, dp, match):
        # Install a higher-priority drop rule for this specific match
        parser = dp.ofproto_parser
        ofp = dp.ofproto
        self.add_flow(dp, priority=200, match=match, actions=[], idle_timeout=60)
        # Optionally remove the lower-priority NORMAL rule to hasten effect
        mod = parser.OFPFlowMod(datapath=dp,
                                command=ofp.OFPFC_DELETE_STRICT,
                                out_port=ofp.OFPP_ANY, out_group=ofp.OFPG_ANY,
                                priority=10, match=match)
        dp.send_msg(mod)
EOF

Run the controller:

source .venv/bin/activate
ryu-manager ai_guard.py

You should see logs about the table-miss rule and stats cycles.


5) Test it: normal vs. “bursty” traffic

Open a second terminal. First, verify connectivity:

sudo ip netns exec h1 ping -c 3 10.0.0.2

Generate steady “normal-ish” traffic (a slow loop of pings):

sudo ip netns exec h1 bash -c 'for i in {1..50}; do ping -c1 -W1 10.0.0.2 >/dev/null; sleep 0.1; done'

Now simulate a burst (many small packets quickly). This crude loop should trip the anomaly detector:

sudo ip netns exec h1 bash -c 'for i in {1..500}; do ping -c1 -W1 10.0.0.2 >/dev/null; done'

Watch the Ryu terminal. You should see log lines like:

WARNING AIGuard: Anomalous flow detected ('10.0.0.1', '10.0.0.2', 1, 0, 0): 250.0 pps, 30000.0 Bps -> dropping

Try ping again and notice it gets blocked (for up to the drop rule idle_timeout):

sudo ip netns exec h1 ping -c 3 10.0.0.2

You can inspect flows and counters at any time:

sudo ovs-ofctl -O OpenFlow13 dump-flows br0

Cleanup when you’re done:

sudo ip netns del h1
sudo ip netns del h2
sudo ip link del br0
sudo ovs-vsctl del-br br0

Real-world tips and patterns

  • Telemetry at scale:

    • OpenFlow counters are fine for a lab. In production, add sFlow or IPFIX/NetFlow exporters and feed an AI pipeline (e.g., Kafka → Python/Scikit/PyTorch).
    • OVS sFlow example:
    sudo ovs-vsctl -- --id=@sflow create sflow agent=eth0 target=\"udp:127.0.0.1:6343\" sampling=200 polling=10 -- set bridge br0 sflow=@sflow
    
  • Safer actuation:

    • Start with “mark + log” instead of “drop.” For example, match anomalies and place them in a lower-priority QoS queue rather than outright blocking.
  • Fallbacks and guardrails:

    • Keep fail-mode secure and maintain an allowlist. Always provide a manual override path and time-bound rules (idle/hard timeouts).
  • Performance:

    • For ultra-low-latency classification, explore eBPF/XDP for feature extraction and in-kernel counters, then make decisions in user space.
  • Scale up:

    • Swap Ryu for ONOS/OVN-Kubernetes/OVSDB for multi-switch setups. Your “brain” can still be a Python microservice that feeds decisions to the controller northbound.

Conclusion and next steps (CTA)

You just stood up an AI-assisted SDN loop on a single Linux box: OVS switches packets, Ryu controls the plane, and a small ML model decides when to intervene. From here:

  • Replace synthetic training with your real baseline data.

  • Add richer features (flow duration, variance, burstiness) and retrain.

  • Try a different action policy (rate-limit instead of drop).

  • Containerize the controller and deploy on a lab fabric with multiple OVS bridges.

If you’d like a follow-up post with sFlow/IPFIX ingestion and a Grafana dashboard, let me know. Until then, iterate on this loop: measure better, decide smarter, act safer.