132  SDN Controller Hands-On Lab

In 60 Seconds

This hands-on lab simulates an SDN controller managing three virtual switches with flow tables on an ESP32 microcontroller. You will process PACKET_IN messages for unknown packets, install FLOW_MOD rules with match fields and actions, configure idle timeouts (10-60s) and hard timeouts (300-3600s) for automatic flow expiration, and track per-flow statistics (packet counts, byte counts, lifetime).

132.1 Learning Objectives

By the end of this lab, you will be able to:

  • Implement SDN Controller Logic: Build a simulated SDN controller managing multiple switches with flow tables on an ESP32
  • Process PACKET_IN Messages: Handle unknown packets, make forwarding decisions, and explain the reactive flow installation pattern
  • Install Flow Rules: Program switch behavior using FLOW_MOD-style messages with match fields, actions, and priority levels
  • Configure Flow Timeouts: Implement idle and hard timeouts for automatic flow expiration and analyze the memory-vs-controller-load tradeoff
  • Collect Flow Statistics: Monitor packet counts, byte counts, and flow lifetimes to verify SDN behavior
MVU: Minimum Viable Understanding

Core concept: This lab demonstrates SDN concepts using an ESP32 microcontroller simulating a controller managing three virtual switches with flow tables. Why it matters: Hands-on experience with reactive flow installation, priority-based QoS, and timeout management reinforces theoretical SDN knowledge. Key takeaway: The first packet of a new flow triggers controller involvement (slow path), but subsequent packets are forwarded at wire-speed using installed rules (fast path).

Software-Defined Networking (SDN) separates the brain of a network (the control plane) from the muscles (the data plane). Think of a traffic management center: instead of each traffic light making its own decisions, a central system monitors all intersections and coordinates them for optimal flow. SDN brings this same centralized intelligence to IoT networks.

132.2 Prerequisites


132.3 Lab Overview

⏱️ ~45 min | ⭐⭐⭐ Advanced | 📋 P04.C28.LAB

Lab Overview

This hands-on lab demonstrates Software-Defined Networking (SDN) concepts using an ESP32 microcontroller. You’ll build a simulated SDN controller that manages packet forwarding decisions for multiple “virtual switches,” showcasing:

  • Centralized Control: Single controller making forwarding decisions for multiple switches
  • Flow Table Management: Dynamic installation and expiration of flow rules
  • Packet-In/Flow-Mod Messages: Simulated OpenFlow protocol communication
  • Priority-Based QoS: Different handling for high-priority vs normal traffic
  • Flow Statistics: Real-time monitoring of packet counts and flow lifetimes
  • Dynamic Reconfiguration: Adding/removing flow rules while network is running

Real-World Parallel: This simulates how an actual SDN controller (like OpenDaylight or ONOS) manages network switches via OpenFlow protocol, but scaled down to demonstrate core concepts on a single microcontroller.


132.4 Interactive Simulation


132.5 Circuit Setup

The ESP32 simulates an SDN controller managing three virtual switches with visual feedback:

Component ESP32 Pin Purpose
Button 1 (Switch 1) GPIO 15 Simulate packet arrival at Switch 1
Button 2 (Switch 2) GPIO 4 Simulate packet arrival at Switch 2
Button 3 (Switch 3) GPIO 5 Simulate packet arrival at Switch 3
Potentiometer GPIO 34 Adjust flow timeout (5-60 seconds)
LED 1 (Blue) GPIO 18 Controller activity indicator
LED 2 (Green) GPIO 19 Flow rule installed successfully
LED 3 (Yellow) GPIO 21 Flow table miss (PACKET_IN)
LED 4 (Red) GPIO 22 Flow rule expired or error
Button 4 (Priority) GPIO 23 Toggle high-priority mode for next flow

Add this diagram.json configuration in Wokwi:

{
  "version": 1,
  "author": "IoT Class - SDN Controller Lab",
  "parts": [
    {"type": "wokwi-esp32-devkit-v1", "id": "esp32", "top": 0, "left": 0, "attrs": {}},
    {"type": "wokwi-pushbutton", "id": "btn1", "top": -48, "left": -96, "attrs": {"color": "blue", "label": "SW1"}},
    {"type": "wokwi-pushbutton", "id": "btn2", "top": -48, "left": 0, "attrs": {"color": "blue", "label": "SW2"}},
    {"type": "wokwi-pushbutton", "id": "btn3", "top": -48, "left": 96, "attrs": {"color": "blue", "label": "SW3"}},
    {"type": "wokwi-pushbutton", "id": "btn4", "top": 192, "left": 96, "attrs": {"color": "orange", "label": "Priority"}},
    {"type": "wokwi-potentiometer", "id": "pot1", "top": 192, "left": -96, "attrs": {"label": "Timeout"}},
    {"type": "wokwi-led", "id": "led1", "top": 384, "left": -144, "attrs": {"color": "blue", "label": "Controller"}},
    {"type": "wokwi-led", "id": "led2", "top": 384, "left": -48, "attrs": {"color": "green", "label": "Rule OK"}},
    {"type": "wokwi-led", "id": "led3", "top": 384, "left": 48, "attrs": {"color": "yellow", "label": "Table Miss"}},
    {"type": "wokwi-led", "id": "led4", "top": 384, "left": 144, "attrs": {"color": "red", "label": "Expired"}}
  ],
  "connections": [
    ["btn1:1.l", "esp32:GND", "black", ["v0"]],
    ["btn1:2.r", "esp32:GPIO15", "blue", ["v0"]],
    ["btn2:1.l", "esp32:GND", "black", ["v0"]],
    ["btn2:2.r", "esp32:GPIO4", "blue", ["v0"]],
    ["btn3:1.l", "esp32:GND", "black", ["v0"]],
    ["btn3:2.r", "esp32:GPIO5", "blue", ["v0"]],
    ["btn4:1.l", "esp32:GND", "black", ["v0"]],
    ["btn4:2.r", "esp32:GPIO23", "orange", ["v0"]],
    ["pot1:GND", "esp32:GND", "black", ["v0"]],
    ["pot1:VCC", "esp32:3V3", "red", ["v0"]],
    ["pot1:SIG", "esp32:GPIO34", "green", ["v0"]],
    ["led1:A", "esp32:GPIO18", "blue", ["v0"]],
    ["led1:C", "esp32:GND", "black", ["v0"]],
    ["led2:A", "esp32:GPIO19", "green", ["v0"]],
    ["led2:C", "esp32:GND", "black", ["v0"]],
    ["led3:A", "esp32:GPIO21", "yellow", ["v0"]],
    ["led3:C", "esp32:GND", "black", ["v0"]],
    ["led4:A", "esp32:GPIO22", "red", ["v0"]],
    ["led4:C", "esp32:GND", "black", ["v0"]]
  ]
}

132.6 Complete Arduino Code

Copy this code into the Wokwi editor.

/*
 * SDN Controller Lab - IoT Class
 * Simulates Software-Defined Networking concepts with ESP32
 *
 * Features:
 * - Centralized flow table management for 3 virtual switches
 * - OpenFlow-style PACKET_IN and FLOW_MOD message simulation
 * - Priority-based QoS (high vs normal priority flows)
 * - Dynamic flow rule installation and timeout management
 * - Real-time statistics and flow lifecycle tracking
 * - Configurable idle and hard timeouts
 *
 * Architecture:
 * - SDN Controller: Processes PACKET_IN, installs flow rules
 * - Flow Tables: Store match-action rules for each switch
 * - Statistics: Track packet counts, byte counts, flow age
 */

#include <Arduino.h>

// Pin Definitions
const int SWITCH1_BTN = 15;
const int SWITCH2_BTN = 4;
const int SWITCH3_BTN = 5;
const int PRIORITY_BTN = 23;
const int TIMEOUT_POT = 34;

const int CONTROLLER_LED = 18;  // Blue - Controller active
const int RULE_OK_LED = 19;     // Green - Rule installed
const int TABLE_MISS_LED = 21;  // Yellow - PACKET_IN (no rule)
const int EXPIRED_LED = 22;     // Red - Flow expired

// Flow Table Configuration
const int MAX_FLOWS_PER_SWITCH = 10;
const int NUM_SWITCHES = 3;

// Flow Entry Structure (OpenFlow-inspired)
struct FlowEntry {
  bool active;               // Is this entry valid?
  uint32_t flowId;          // Unique flow identifier
  uint8_t srcMac[6];        // Source MAC address (match field)
  uint8_t dstMac[6];        // Destination MAC address (match field)
  uint16_t priority;        // Priority (higher = checked first)
  uint8_t action;           // Action: 0=DROP, 1=FORWARD, 2=CONTROLLER
  uint8_t outputPort;       // Output port number (if FORWARD)
  unsigned long installTime;     // When rule was installed
  unsigned long lastMatchTime;   // Last packet match time
  unsigned long idleTimeout;     // Idle timeout (ms) - remove if no matches
  unsigned long hardTimeout;     // Hard timeout (ms) - remove regardless
  uint32_t packetCount;     // Number of packets matched
  uint32_t byteCount;       // Total bytes matched
  uint8_t cookie;           // Controller-set identifier
};

// Global Flow Tables (one per switch)
FlowEntry flowTables[NUM_SWITCHES][MAX_FLOWS_PER_SWITCH];

// Simulated Packet Structure
struct Packet {
  uint8_t srcMac[6];
  uint8_t dstMac[6];
  uint16_t etherType;
  uint32_t srcIp;
  uint32_t dstIp;
  uint16_t srcPort;
  uint16_t dstPort;
  uint8_t protocol;
  uint16_t length;
  bool highPriority;
};

// Statistics
struct ControllerStats {
  uint32_t packetInCount;
  uint32_t flowModCount;
  uint32_t totalFlowsInstalled;
  uint32_t totalFlowsExpired;
  uint32_t totalPacketsForwarded;
  unsigned long uptime;
} stats;

// State Variables
bool highPriorityMode = false;
uint32_t nextFlowId = 1;
unsigned long lastStatsDisplay = 0;
unsigned long lastTimeoutCheck = 0;

// Function Prototypes
void initializeFlowTables();
void handleSwitchButton(int switchId);
void processPacketIn(int switchId, Packet& pkt);
void installFlowRule(int switchId, Packet& pkt, uint8_t action, uint8_t outputPort);
int matchFlowEntry(int switchId, Packet& pkt);
void removeFlowEntry(int switchId, int entryIndex);
void checkFlowTimeouts();
void displayStatistics();
void generatePacket(Packet& pkt, int switchId);
void blinkLED(int pin, int duration);
String macToString(uint8_t* mac);
String ipToString(uint32_t ip);

void setup() {
  Serial.begin(115200);
  Serial.println("\n\n========================================================");
  Serial.println("      SDN Controller Lab - IoT Class                    ");
  Serial.println("      Simulating Software-Defined Networking            ");
  Serial.println("========================================================\n");

  // Initialize pins
  pinMode(SWITCH1_BTN, INPUT_PULLUP);
  pinMode(SWITCH2_BTN, INPUT_PULLUP);
  pinMode(SWITCH3_BTN, INPUT_PULLUP);
  pinMode(PRIORITY_BTN, INPUT_PULLUP);
  pinMode(TIMEOUT_POT, INPUT);

  pinMode(CONTROLLER_LED, OUTPUT);
  pinMode(RULE_OK_LED, OUTPUT);
  pinMode(TABLE_MISS_LED, OUTPUT);
  pinMode(EXPIRED_LED, OUTPUT);

  // Initialize flow tables
  initializeFlowTables();

  // Initialize statistics
  memset(&stats, 0, sizeof(stats));

  Serial.println("SDN Controller initialized");
  Serial.println("   - Managing 3 virtual switches");
  Serial.println("   - Flow table capacity: 10 rules per switch");
  Serial.println("   - OpenFlow-style reactive flow installation\n");

  Serial.println("Controls:");
  Serial.println("   - Button SW1/SW2/SW3: Simulate packet arrival at switch");
  Serial.println("   - Button Priority: Toggle high-priority mode");
  Serial.println("   - Potentiometer: Adjust flow timeout (5-60 seconds)\n");

  Serial.println("LED Indicators:");
  Serial.println("   - Blue: Controller processing");
  Serial.println("   - Green: Flow rule installed successfully");
  Serial.println("   - Yellow: Table miss (PACKET_IN to controller)");
  Serial.println("   - Red: Flow rule expired\n");

  Serial.println("Ready! Press any switch button to begin...\n");

  // Power-on self-test
  digitalWrite(CONTROLLER_LED, HIGH);
  delay(200);
  digitalWrite(RULE_OK_LED, HIGH);
  delay(200);
  digitalWrite(TABLE_MISS_LED, HIGH);
  delay(200);
  digitalWrite(EXPIRED_LED, HIGH);
  delay(200);
  digitalWrite(CONTROLLER_LED, LOW);
  digitalWrite(RULE_OK_LED, LOW);
  digitalWrite(TABLE_MISS_LED, LOW);
  digitalWrite(EXPIRED_LED, LOW);
}

void loop() {
  // Check switch buttons
  if (digitalRead(SWITCH1_BTN) == LOW) {
    handleSwitchButton(0);
    delay(300); // Debounce
  }
  if (digitalRead(SWITCH2_BTN) == LOW) {
    handleSwitchButton(1);
    delay(300);
  }
  if (digitalRead(SWITCH3_BTN) == LOW) {
    handleSwitchButton(2);
    delay(300);
  }

  // Check priority toggle
  if (digitalRead(PRIORITY_BTN) == LOW) {
    highPriorityMode = !highPriorityMode;
    Serial.println(highPriorityMode ? "HIGH PRIORITY MODE ENABLED" : "Normal priority mode");
    blinkLED(highPriorityMode ? EXPIRED_LED : CONTROLLER_LED, 500);
    delay(300);
  }

  // Check flow timeouts every second
  if (millis() - lastTimeoutCheck > 1000) {
    checkFlowTimeouts();
    lastTimeoutCheck = millis();
  }

  // Display statistics every 10 seconds
  if (millis() - lastStatsDisplay > 10000) {
    displayStatistics();
    lastStatsDisplay = millis();
  }

  stats.uptime = millis() / 1000;
}

void initializeFlowTables() {
  for (int sw = 0; sw < NUM_SWITCHES; sw++) {
    for (int i = 0; i < MAX_FLOWS_PER_SWITCH; i++) {
      flowTables[sw][i].active = false;
      flowTables[sw][i].flowId = 0;
      flowTables[sw][i].priority = 0;
      flowTables[sw][i].packetCount = 0;
      flowTables[sw][i].byteCount = 0;
    }
  }
}

void handleSwitchButton(int switchId) {
  digitalWrite(CONTROLLER_LED, HIGH);

  Serial.println("===========================================================");
  Serial.printf("PACKET ARRIVAL at Switch %d\n", switchId + 1);

  // Generate simulated packet
  Packet pkt;
  generatePacket(pkt, switchId);

  Serial.println("\nPacket Details:");
  Serial.printf("   Src MAC: %s\n", macToString(pkt.srcMac).c_str());
  Serial.printf("   Dst MAC: %s\n", macToString(pkt.dstMac).c_str());
  Serial.printf("   Src IP:  %s\n", ipToString(pkt.srcIp).c_str());
  Serial.printf("   Dst IP:  %s\n", ipToString(pkt.dstIp).c_str());
  Serial.printf("   Protocol: %d, Length: %d bytes\n", pkt.protocol, pkt.length);
  Serial.printf("   Priority: %s\n", pkt.highPriority ? "HIGH" : "NORMAL");

  // Try to match against flow table
  int matchedEntry = matchFlowEntry(switchId, pkt);

  if (matchedEntry >= 0) {
    // Flow table HIT - Execute action locally (fast path)
    FlowEntry* entry = &flowTables[switchId][matchedEntry];

    Serial.println("\nFLOW TABLE HIT (Hardware Forwarding)");
    Serial.printf("   Matched Flow ID: %d (Priority: %d)\n", entry->flowId, entry->priority);
    Serial.printf("   Action: %s", entry->action == 0 ? "DROP" :
                                    entry->action == 1 ? "FORWARD" : "TO_CONTROLLER");
    if (entry->action == 1) {
      Serial.printf(" to port %d", entry->outputPort);
    }
    Serial.println();

    // Update statistics
    entry->packetCount++;
    entry->byteCount += pkt.length;
    entry->lastMatchTime = millis();
    stats.totalPacketsForwarded++;

    Serial.printf("   Flow Stats: %d packets, %d bytes, age: %lu seconds\n",
                  entry->packetCount, entry->byteCount,
                  (millis() - entry->installTime) / 1000);

    // Visual feedback: quick green blink (fast forwarding)
    blinkLED(RULE_OK_LED, 100);

  } else {
    // Flow table MISS - Send PACKET_IN to controller (slow path)
    Serial.println("\nFLOW TABLE MISS -> PACKET_IN to Controller");
    Serial.println("   (First packet of new flow - requires controller decision)");

    digitalWrite(TABLE_MISS_LED, HIGH);
    stats.packetInCount++;

    // Simulate controller processing delay
    delay(50); // SDN controller round-trip time (5-50ms typical)

    // Controller processes packet and installs flow rule
    processPacketIn(switchId, pkt);

    digitalWrite(TABLE_MISS_LED, LOW);
  }

  Serial.println("===========================================================\n");
  digitalWrite(CONTROLLER_LED, LOW);
}

void processPacketIn(int switchId, Packet& pkt) {
  Serial.println("\nSDN CONTROLLER PROCESSING:");
  Serial.println("   1. Analyze packet headers");
  Serial.println("   2. Check application policies");
  Serial.println("   3. Compute optimal forwarding path");
  Serial.println("   4. Install flow rule via FLOW_MOD");

  // Simulated forwarding logic (in real SDN, this comes from application layer)
  uint8_t action = 1;  // FORWARD
  uint8_t outputPort = (switchId + 1) % 3 + 1; // Round-robin ports

  // High-priority traffic gets different treatment
  if (pkt.highPriority) {
    Serial.println("\nHIGH PRIORITY TRAFFIC DETECTED");
    Serial.println("   - Using dedicated low-latency path");
    Serial.println("   - Installing rule with high priority (1000)");
    outputPort = 1; // Dedicated high-priority port
  }

  // Install flow rule
  installFlowRule(switchId, pkt, action, outputPort);

  // Visual feedback
  blinkLED(RULE_OK_LED, 300);
}

void installFlowRule(int switchId, Packet& pkt, uint8_t action, uint8_t outputPort) {
  // Find empty slot in flow table
  int emptySlot = -1;
  for (int i = 0; i < MAX_FLOWS_PER_SWITCH; i++) {
    if (!flowTables[switchId][i].active) {
      emptySlot = i;
      break;
    }
  }

  if (emptySlot < 0) {
    Serial.println("\nERROR: Flow table full! (In real SDN, controller would evict oldest rule)");
    return;
  }

  // Read timeout from potentiometer (5-60 seconds)
  int potValue = analogRead(TIMEOUT_POT);
  int timeoutSeconds = map(potValue, 0, 4095, 5, 60);
  unsigned long idleTimeout = timeoutSeconds * 1000;
  unsigned long hardTimeout = timeoutSeconds * 2 * 1000; // Hard timeout = 2x idle

  // Create flow entry
  FlowEntry* entry = &flowTables[switchId][emptySlot];
  entry->active = true;
  entry->flowId = nextFlowId++;
  memcpy(entry->srcMac, pkt.srcMac, 6);
  memcpy(entry->dstMac, pkt.dstMac, 6);
  entry->priority = pkt.highPriority ? 1000 : 100; // High priority rules checked first
  entry->action = action;
  entry->outputPort = outputPort;
  entry->installTime = millis();
  entry->lastMatchTime = millis();
  entry->idleTimeout = idleTimeout;
  entry->hardTimeout = hardTimeout;
  entry->packetCount = 1; // Count this first packet
  entry->byteCount = pkt.length;
  entry->cookie = switchId; // Controller identifier

  stats.flowModCount++;
  stats.totalFlowsInstalled++;
  stats.totalPacketsForwarded++;

  Serial.println("\nFLOW_MOD Message Sent:");
  Serial.printf("   Switch: %d, Flow ID: %d\n", switchId + 1, entry->flowId);
  Serial.printf("   Match: Src=%s, Dst=%s\n",
                macToString(entry->srcMac).c_str(),
                macToString(entry->dstMac).c_str());
  Serial.printf("   Action: %s to port %d\n",
                action == 1 ? "FORWARD" : "DROP", outputPort);
  Serial.printf("   Priority: %d\n", entry->priority);
  Serial.printf("   Timeouts: Idle=%d s, Hard=%d s\n",
                timeoutSeconds, timeoutSeconds * 2);
  Serial.println("   Flow rule installed in switch hardware");
}

int matchFlowEntry(int switchId, Packet& pkt) {
  int bestMatch = -1;
  uint16_t highestPriority = 0;

  // Check all active flows, prioritize by priority field
  for (int i = 0; i < MAX_FLOWS_PER_SWITCH; i++) {
    FlowEntry* entry = &flowTables[switchId][i];

    if (!entry->active) continue;

    // Match on source and destination MAC (simplified - real OpenFlow matches many fields)
    bool srcMatch = memcmp(entry->srcMac, pkt.srcMac, 6) == 0;
    bool dstMatch = memcmp(entry->dstMac, pkt.dstMac, 6) == 0;

    if (srcMatch && dstMatch) {
      // Found a match - check if higher priority than previous matches
      if (entry->priority > highestPriority) {
        bestMatch = i;
        highestPriority = entry->priority;
      }
    }
  }

  return bestMatch;
}

void removeFlowEntry(int switchId, int entryIndex) {
  FlowEntry* entry = &flowTables[switchId][entryIndex];

  Serial.println("\nFLOW REMOVAL:");
  Serial.printf("   Switch: %d, Flow ID: %d\n", switchId + 1, entry->flowId);
  Serial.printf("   Lifetime: %lu seconds\n", (millis() - entry->installTime) / 1000);
  Serial.printf("   Final Stats: %d packets, %d bytes\n",
                entry->packetCount, entry->byteCount);

  entry->active = false;
  stats.totalFlowsExpired++;

  blinkLED(EXPIRED_LED, 200);
}

void checkFlowTimeouts() {
  unsigned long now = millis();

  for (int sw = 0; sw < NUM_SWITCHES; sw++) {
    for (int i = 0; i < MAX_FLOWS_PER_SWITCH; i++) {
      FlowEntry* entry = &flowTables[sw][i];

      if (!entry->active) continue;

      // Check hard timeout (unconditional expiration)
      if (now - entry->installTime > entry->hardTimeout) {
        Serial.printf("\nHard timeout expired for Flow %d on Switch %d\n",
                      entry->flowId, sw + 1);
        removeFlowEntry(sw, i);
        continue;
      }

      // Check idle timeout (no packets for X seconds)
      if (now - entry->lastMatchTime > entry->idleTimeout) {
        Serial.printf("\nIdle timeout expired for Flow %d on Switch %d\n",
                      entry->flowId, sw + 1);
        Serial.printf("   (No packets matched for %lu seconds)\n",
                      entry->idleTimeout / 1000);
        removeFlowEntry(sw, i);
      }
    }
  }
}

void displayStatistics() {
  Serial.println("\n========================================================");
  Serial.println("              SDN CONTROLLER STATISTICS                 ");
  Serial.println("========================================================");
  Serial.printf("Uptime: %lu seconds\n\n", stats.uptime);

  Serial.println("Message Counts:");
  Serial.printf("   PACKET_IN:  %d (table misses -> controller)\n", stats.packetInCount);
  Serial.printf("   FLOW_MOD:   %d (rules installed by controller)\n", stats.flowModCount);
  Serial.printf("   Ratio:      %.2f PACKET_IN per FLOW_MOD\n\n",
                stats.flowModCount > 0 ? (float)stats.packetInCount / stats.flowModCount : 0);

  Serial.println("Flow Statistics:");
  Serial.printf("   Total Installed: %d\n", stats.totalFlowsInstalled);
  Serial.printf("   Total Expired:   %d\n", stats.totalFlowsExpired);
  Serial.printf("   Currently Active: %d\n", stats.totalFlowsInstalled - stats.totalFlowsExpired);
  Serial.printf("   Total Packets:   %d\n\n", stats.totalPacketsForwarded);

  Serial.println("Active Flow Tables:");
  for (int sw = 0; sw < NUM_SWITCHES; sw++) {
    int activeCount = 0;
    for (int i = 0; i < MAX_FLOWS_PER_SWITCH; i++) {
      if (flowTables[sw][i].active) activeCount++;
    }
    Serial.printf("   Switch %d: %d/%d entries used\n",
                  sw + 1, activeCount, MAX_FLOWS_PER_SWITCH);

    // Show details of active flows
    for (int i = 0; i < MAX_FLOWS_PER_SWITCH; i++) {
      if (flowTables[sw][i].active) {
        FlowEntry* e = &flowTables[sw][i];
        unsigned long age = (millis() - e->installTime) / 1000;
        unsigned long idle = (millis() - e->lastMatchTime) / 1000;
        Serial.printf("      Flow %d: %d pkts, %d bytes, age=%lu s, idle=%lu s, prio=%d\n",
                      e->flowId, e->packetCount, e->byteCount, age, idle, e->priority);
      }
    }
  }
  Serial.println("========================================================\n");
}

void generatePacket(Packet& pkt, int switchId) {
  // Generate pseudo-random but realistic MAC addresses
  uint32_t rnd = millis() + switchId * 1000 + random(0, 1000);

  pkt.srcMac[0] = 0x00;
  pkt.srcMac[1] = 0x1A + switchId;
  pkt.srcMac[2] = (rnd >> 24) & 0x0F; // Limited randomness to get repeated flows
  pkt.srcMac[3] = (rnd >> 16) & 0xFF;
  pkt.srcMac[4] = (rnd >> 8) & 0xFF;
  pkt.srcMac[5] = rnd & 0xFF;

  pkt.dstMac[0] = 0x00;
  pkt.dstMac[1] = 0x1B;
  pkt.dstMac[2] = 0x2C;
  pkt.dstMac[3] = 0x3D;
  pkt.dstMac[4] = switchId + 1;
  pkt.dstMac[5] = 0x01;

  pkt.etherType = 0x0800; // IPv4
  pkt.srcIp = (10 << 24) | (0 << 16) | (switchId << 8) | (random(1, 254));
  pkt.dstIp = (192 << 24) | (168 << 16) | (1 << 8) | 100;
  pkt.srcPort = 1024 + random(0, 60000);
  pkt.dstPort = 80; // HTTP
  pkt.protocol = 6; // TCP
  pkt.length = random(64, 1500);
  pkt.highPriority = highPriorityMode;

  // Reset high priority mode after one packet
  if (highPriorityMode) {
    highPriorityMode = false;
  }
}

void blinkLED(int pin, int duration) {
  digitalWrite(pin, HIGH);
  delay(duration);
  digitalWrite(pin, LOW);
}

String macToString(uint8_t* mac) {
  char buf[18];
  snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X",
           mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  return String(buf);
}

String ipToString(uint32_t ip) {
  char buf[16];
  snprintf(buf, sizeof(buf), "%d.%d.%d.%d",
           (ip >> 24) & 0xFF, (ip >> 16) & 0xFF,
           (ip >> 8) & 0xFF, ip & 0xFF);
  return String(buf);
}

132.7 What You’ll Observe

Normal Operation (Reactive Flow Installation):

  1. First Packet -> Table miss (Yellow LED) -> Controller installs rule (Blue LED) -> Rule confirmed (Green LED)
  2. Subsequent Packets -> Table hit -> Fast hardware forwarding (Quick green blink, no controller involvement)
  3. Flow Expiration -> Red LED blinks when idle/hard timeout expires
  4. Statistics -> Serial monitor shows flow counts, packet counts, and active flows

Key SDN Concepts Demonstrated:

  • Separation of Control and Data Planes: Controller (brain) makes decisions, switches (muscles) forward packets
  • Reactive vs Proactive: First packet triggers controller involvement (reactive), subsequent packets use installed rules (fast path)
  • Flow Table Priority: High-priority flows (Priority button) use priority=1000, normal flows use priority=100
  • Timeout Management: Adjust potentiometer to see how shorter timeouts cause more PACKET_IN messages (controller load)
  • Statistics Collection: Real-time counters for packets, bytes, and flow lifetimes (OpenFlow STATS_REQUEST)

132.8 Challenge Exercises

Objective: Modify the code to pre-install flow rules for common traffic patterns before packets arrive.

Task: In setup(), add code to install proactive flow rules for all three switches with destination MAC 00:1B:2C:3D:01:01, forwarding to port 2 with priority 500.

Why This Matters: Real SDN controllers use proactive installation for predictable traffic (e.g., IoT sensors reporting to known gateways) to avoid first-packet latency and reduce controller load.

Expected Outcome: When you press a switch button and the generated packet happens to match the pre-installed rule, you should see immediate table hit (no yellow LED) even for “first” packet.

Objective: Implement least-recently-used (LRU) eviction when flow table is full.

Task: Modify installFlowRule() to find and remove the flow with the oldest lastMatchTime when no empty slots are available, rather than rejecting the new flow.

Why This Matters: OpenFlow switches have limited TCAM space (e.g., 4000 entries). Controllers must evict unused flows to make room for active traffic. LRU is a common strategy.

Expected Outcome: When all 10 slots are full, installing an 11th flow should evict the least-recently-matched flow. Serial monitor should show “Evicting flow X to make room for flow Y”.

Objective: Implement a security policy that drops traffic from specific source MACs.

Task: Add a button (GPIO 27) labeled “Block” that installs a DROP rule (action=0) for the last packet’s source MAC with priority=2000 (higher than normal flows). Modify match logic to honor DROP action.

Why This Matters: SDN enables dynamic security - controllers can instantly block malicious sources network-wide by installing high-priority drop rules.

Expected Outcome: After pressing “Block”, subsequent packets from that source MAC should show “Flow table hit -> ACTION: DROP” and not increment packet/byte counters.

Objective: Simulate OpenFlow group tables for load balancing across multiple output ports.

Task: Create a GroupTable structure with multiple outputPorts[]. When installing a flow rule, reference a group ID instead of single port. Randomly select from group’s ports each time rule is matched.

Why This Matters: OpenFlow group tables enable multicast, fast-failover, and load balancing. Server load balancers use SELECT groups to distribute connections across backend servers.

Expected Outcome: Packets matching the rule should be forwarded to port 1, then port 2, then port 3, cycling through the group. Serial monitor shows “Forwarding to port X (group ID 42, bucket 2/5)”.

Objective: Simulate a backup controller that takes over when primary fails.

Task: Add a “Fail Controller” button that sets controllerAvailable = false for 10 seconds. During this time, existing flows continue matching, but new PACKET_IN messages should fail with “Controller unreachable - buffering packet”. After 10s, simulate backup takeover and process buffered packets.

Why This Matters: SDN controllers are critical single points of failure. Production deployments use clustered controllers (ONOS, OpenDaylight) with leader election. Understanding data plane resilience during control plane outages is essential.

Expected Outcome: Existing flows continue forwarding (green LED). New flows fail (red LED + error message). After recovery, buffered packets are processed and rules installed.


132.9 Expected Outcomes

You should observe:

  1. Reactive Flow Installation: First packet triggers controller (yellow -> blue -> green sequence), subsequent packets use fast path (quick green only)
  2. Priority Behavior: High-priority flows (press Priority button) get priority=1000 and dedicated port 1, normal flows use priority=100 and round-robin ports
  3. Timeout Management: With potentiometer at minimum (5s), flows expire quickly causing more PACKET_IN messages. At maximum (60s), flows persist longer reducing controller load
  4. Statistics Growth: Every 10 seconds, statistics show increasing PACKET_IN and FLOW_MOD counts, tracking flow lifecycle
  5. Flow Table State: Active flows display shows packet counts increasing for matched flows, idle time resetting on each match
  6. Controller Efficiency: Ratio of PACKET_IN to FLOW_MOD approaches 1:1 for diverse traffic (each new source triggers one rule installation), but much lower if same source sends multiple packets (1:many - efficient caching)

Real-World Parallels:

  • Controller = OpenDaylight/ONOS: Central brain processing PACKET_IN and sending FLOW_MOD
  • Switch = Hardware OpenFlow Switch: Forwarding at Gbps using TCAM lookups
  • Flow Table = TCAM: High-speed hardware memory for match-action rules
  • Priority = Flow Priority Field: Higher priority rules checked first (security before routing)
  • Timeouts = Idle/Hard Timeouts: Automatic garbage collection of unused flows
  • Statistics = STATS_REPLY: Controllers poll switch counters for monitoring and billing

132.10 Learning Insights

Key Takeaways:

  1. Centralized Control, Distributed Forwarding: Controller makes smart decisions, switches execute them fast. Best of both worlds.

  2. Reactive Flow Installation: First packet pays latency penalty (controller round-trip ~50ms), but subsequent packets achieve wire-speed forwarding. This is core SDN tradeoff - programmability vs first-packet latency.

In the lab, Button 1 triggers a simulated PACKET_IN message to the ESP32 controller. The controller processes the message and installs a flow rule via FLOW_MOD. Timing breakdown:

\[\text{PACKET_IN Processing} = 50 \text{ ms (simulated controller round-trip)}\] \[\text{Flow Rule Installation} = \text{FLOW_MOD message} + \text{TCAM write} \approx 5 \text{ ms}\] \[\text{First Packet Total Latency} = 50 + 5 = 55 \text{ ms}\]

Subsequent packets matching the installed rule use hardware forwarding:

\[\text{Subsequent Packet Latency} = \text{TCAM lookup (10 ns)} + \text{switch fabric} + \text{serialization} \approx 50 \text{ microseconds}\]

Speedup for 1000 packets in the same flow: First packet takes 55ms, remaining 999 take \(999 \times 0.05 = 49.95\) ms. Total: 104.95 ms. Without SDN (all packets go to controller): \(1000 \times 55 = 55,000\) ms. SDN achieves 524× faster processing for bulk flows.

  1. Priority Matters: High-priority rules (security, QoS) must have higher priority values than general routing rules. Order determines which rule wins when multiple match.

  2. Timeout Tuning: Short timeouts reduce memory usage but increase controller load. Long timeouts reduce controller messages but waste memory on stale flows. Typical defaults: idle=10s, hard=300s.

  3. Controller Failure Resilience: Data plane continues working during control plane outage (existing flows unaffected), but new flows cannot be established. This is why controller clustering is critical for production.

  4. Scalability Limits: Flow table size limits how many concurrent flows a switch can handle. Controllers must implement eviction policies (LRU, idle, priority-based) when tables fill. This is a real constraint in hardware switches (typically 1000-10000 TCAM entries).


132.11 Knowledge Check

132.12 Connection to Real SDN Deployments

This lab simulates:

Lab Component Real SDN Equivalent
ESP32 Controller OpenDaylight, ONOS, Ryu controller (Java/Python software)
Virtual Switches OpenFlow hardware switches (Pica8, NoviFlow, Arista) or Open vSwitch
PACKET_IN Ethernet frame sent from switch to controller when no rule matches
FLOW_MOD OpenFlow message installing match-action rule in switch TCAM
Flow Table TCAM (Ternary Content Addressable Memory) hardware in switch
Priority Field OpenFlow priority (0-65535, higher = checked first)
Idle/Hard Timeout OpenFlow idle_timeout and hard_timeout fields in flow entry
Statistics OpenFlow STATS_REQUEST/REPLY messages (polled by controller)

Real-world use cases:

  • Data Center: Facebook uses SDN to dynamically route traffic between servers based on load, avoiding congested links
  • Smart City: SDN enables traffic prioritization (emergency vehicles get green lights, video surveillance gets guaranteed bandwidth)
  • Carrier Network: AT&T uses SDN to provision customer connections in minutes instead of weeks
  • IoT Network: Smart building uses SDN to isolate IoT devices from corporate network, enforcing security policies centrally

Scenario: A smart building network has 500 IoT devices (sensors, cameras, actuators) managed by an SDN controller with 8 OpenFlow switches. Each device generates traffic patterns requiring flow rules. You need to size the TCAM capacity for each switch.

Given Data:

  • 500 IoT devices distributed across 8 switches (~62 devices per switch)
  • Traffic patterns: 60% periodic telemetry (predictable), 30% event-driven (unpredictable), 10% command/response
  • Average flow duration: telemetry 60s, events 10s, commands 5s
  • Target: support burst traffic of 2x normal without controller overload

Step 1: Calculate Base Flow Requirements

Each device generates multiple concurrent flows: - Telemetry: 1 uplink flow per device per 60s - Events: 0.3 flows per device (30% probability at any moment) - Commands: 0.1 flows per device (10% probability)

Base flows per switch: 62 devices × (1 + 0.3 + 0.1) = 62 × 1.4 = 87 concurrent flows

Step 2: Add Burst Capacity

During peak events (fire alarm, security breach), traffic doubles: - Burst capacity: 87 × 2 = 174 flows

Step 3: Account for Flow Table Churn

Flows expire and get recreated. For 10s event flows: - Churn rate: 60s / 10s = 6 flow installations per minute per event device - With 30% event devices: 62 × 0.3 × 6 = 112 PACKET_IN/min per switch

Step 4: Size TCAM with Safety Margin

Minimum TCAM entries: 174 flows + 20% safety margin = 209 entries

Commercial switch selection: - Low-cost switch (1000 TCAM entries): 209/1000 = 21% utilization ✓ Acceptable - If using wildcard aggregation (5:1 ratio): 209/5 = 42 unique rules → 4% utilization ✓ Excellent

Step 5: Validate Controller Load

Total PACKET_IN messages across 8 switches: - 8 switches × 112 PACKET_IN/min = 896 PACKET_IN/min = 14.9 msg/sec

OpenDaylight controller capacity: ~10,000 flow setups/sec → 0.15% loadNo bottleneck

Decision: Deploy low-cost switches (1000 TCAM) with wildcard aggregation. The 4% TCAM utilization leaves headroom for 20x traffic growth before needing switch upgrades.

When configuring flow rules, idle and hard timeouts determine when entries expire. This table helps select appropriate values based on IoT traffic patterns.

Traffic Type Characteristics Recommended Idle Timeout Recommended Hard Timeout Rationale
Sensor Telemetry Regular intervals (30-60s), predictable 90-120s 600-1800s Slightly longer than interval to cache repeating flows; hard timeout forces re-authorization hourly
Video Streams Long-lived, high data rate 300s (5 min) No limit (0) Video sessions last hours; idle timeout detects dead streams; no hard timeout avoids disruption
Event Alerts Burst traffic, short-lived 10-20s 60-120s Events are transient; short idle detects end quickly; short hard timeout prevents stale rules
Firmware Updates Bulk transfer, intermittent 60s 3600s (1 hour) Downloads have pauses; moderate idle tolerates gaps; hard timeout prevents forgotten transfers
Command/Response Interactive, low latency 5-10s 30-60s Commands expect quick reply; short idle detects failure; short hard timeout for security

Tuning Guidelines:

  • Idle timeout < transmission interval: Flow expires between packets → unnecessary controller load
  • Idle timeout >> transmission interval: Stale flows waste TCAM → memory pressure
  • Hard timeout for security: Force periodic re-authentication (e.g., 5-15 minutes) even if flow is active
  • Monitoring: Track flow_expired_idle vs flow_expired_hard metrics; if 90%+ expire hard, timeouts too long
Common Mistake: Forgetting PACKET_IN Rate Limiting

The Problem: A firmware bug caused 100 sensors to retry failed connections every 100ms (instead of 60s), generating 1,000 PACKET_IN/sec. Without rate limiting, the controller CPU saturated at 95%, causing it to drop PACKET_IN messages from ALL 500 devices, including healthy ones.

Why It Happens: Developers focus on “happy path” flow installation but don’t plan for misbehaving devices. A single buggy firmware can create a retry storm that brings down the entire network.

The Solution: Implement per-device PACKET_IN rate limiting at the controller:

# OpenDaylight rate limiter configuration
rate_limit:
  per_device:
    max_rate: 5   # PACKET_IN per second per device
    burst: 10     # Allow short bursts
  global:
    max_rate: 500 # Total PACKET_IN per second

Configuration Rules:

  • Per-device limit: 5 msg/sec is generous for IoT (most devices << 1 msg/sec)
  • Burst allowance: 10 messages handles connection setup bursts
  • Global limit: Protect controller from distributed attack (500 devices × 1 msg/sec typical)
  • Response to violators: Drop excess PACKET_IN, log device MAC for debugging, optionally blacklist repeat offenders

Real-World Impact: After implementing rate limiting, a similar bug affected only the 100 buggy sensors (throttled to 5 msg/sec each = 500 total), while the remaining 400 sensors continued normal operation. Controller CPU stayed at 30%.


132.13 Concept Relationships

This Concept Relates To Relationship Type Why It Matters
PACKET_IN Message Flow Table Miss Trigger/Response When switch has no matching flow rule, it generates PACKET_IN to controller - this is the entry point to reactive flow installation
FLOW_MOD Message Flow Table Entry Installation/Programming Controller sends FLOW_MOD to install match-action rules in switch TCAM - this is how control plane programs data plane
Idle Timeout Hard Timeout Resource Management/Tradeoff Idle timeout removes unused flows (saves TCAM), hard timeout forces re-authorization (security) - together they manage flow lifecycle
Priority Field Flow Matching Order Conflict Resolution/QoS Higher priority rules checked first (1000 vs 100) - enables security rules to override routing, QoS to override best-effort
Flow Statistics Controller Monitoring Feedback/Optimization Switch maintains packet/byte counters per flow - controller polls these for billing, anomaly detection, traffic engineering

132.14 See Also

Next Steps - Deep Dives:

Related Concepts:


Key Concepts

  • SDN (Software-Defined Networking): An architectural approach separating the network control plane (routing decisions) from the data plane (packet forwarding), centralizing control in a software controller for programmable network management
  • Control Plane: The network intelligence layer making routing and forwarding decisions, centralized in an SDN controller rather than distributed across individual switches as in traditional networking
  • Data Plane: The network forwarding layer physically moving packets based on rules installed by the control plane — in SDN, this is the switch hardware executing OpenFlow flow table entries
  • OpenFlow: The foundational SDN protocol enabling communication between an SDN controller and network switches, allowing the controller to install, modify, and delete flow table entries that govern packet forwarding
  • SDN Controller: The centralized network operating system providing a global view of the network topology and programming switch forwarding behavior through southbound APIs (OpenFlow) and exposing northbound APIs to applications
  • Mininet: A network emulator running real Linux kernel networking software in a lightweight virtual environment, enabling SDN experiments with hundreds of virtual switches and hosts on a laptop
  • OVS (Open vSwitch): An open-source multilayer virtual switch implementing OpenFlow and supporting SDN controller integration, widely used in both Mininet emulation and production data center SDN deployments

Common Pitfalls

Running SDN lab experiments without pinning controller, switch firmware, and OpenFlow library versions. Version mismatches between Mininet, OVS, and the SDN controller cause unexpected behavior that wastes debugging time. Use Docker containers with fixed dependency versions for reproducible lab environments.

Installing flow rules without verifying their effect on packet forwarding using ping, traceroute, or flow statistics. A flow rule with incorrect priority or match field silently fails to match intended traffic. Always verify connectivity after every flow table modification.

Completing lab exercises only under normal conditions without testing controller failure scenarios. Real SDN deployments must survive controller crashes, switch reconnects, and rule conflicts. Include at least one failure injection test (kill the controller, disconnect a switch) in every lab exercise.

Starting a new lab exercise without clearing all flow rules from the previous exercise. Residual flow rules from earlier exercises match unexpected traffic in the new exercise, causing confusing behavior. Always run “delete-flows” or restart the SDN controller at the beginning of each exercise.

132.15 Summary

This lab demonstrated Software-Defined Networking concepts through hands-on ESP32 simulation:

  • Centralized Control: Single controller managing multiple switches with flow tables
  • Reactive Flow Installation: First packet triggers PACKET_IN to controller, which installs flow rules via FLOW_MOD
  • Priority-Based QoS: High-priority flows (priority=1000) take precedence over normal flows (priority=100)
  • Timeout Management: Idle timeouts remove unused flows; hard timeouts force periodic re-authorization
  • Statistics Collection: Real-time tracking of packet counts, byte counts, and flow lifetimes
  • Data Plane Resilience: Existing flows continue during controller outage; only new flows fail

Deep Dives:

Comparisons:

Applications:

Learning:

Building an SDN controller is like building a robot that manages all the traffic lights in a city!

132.15.1 The Sensor Squad Adventure: The Controller Workshop

The Sensor Squad decided to build their OWN SDN controller using an ESP32 microcontroller! It was like a science fair project, but for networks.

“Our controller will manage three virtual switches,” said Max the Microcontroller proudly. “Each switch has a flow table – like a recipe book that tells it what to do with different messages.”

Sammy the Sensor pressed Button 1 to simulate a message arriving at Switch 1. The yellow LED blinked – “TABLE MISS! The switch doesn’t know what to do!” Then the blue LED lit up – “The controller is thinking!” Then the green LED blinked – “A new rule has been installed!”

“Now press the button again!” said Lila the LED. This time, only the green LED blinked quickly – “TABLE HIT! The switch remembered the rule and forwarded the message instantly, without asking the controller!”

Bella the Battery turned the potentiometer to adjust how long rules stayed in the switch. “If I set it to 5 seconds, rules expire quickly – the switch has to ask the controller more often. If I set it to 60 seconds, rules last longer and the controller gets a break!”

The orange button was the most exciting: “PRIORITY MODE!” When pressed, the next message got VIP treatment – a special fast lane through the network!

The squad learned the most important SDN lesson: the first message is slow (the controller has to think), but every message after that is SUPER fast (the switch already knows what to do)!

132.15.2 Key Words for Kids

Word What It Means
Flow Table A list of rules stored in each switch, like a recipe book
Table Miss When a switch gets a message it does not have a rule for (needs help from controller)
Table Hit When a switch finds a matching rule and handles the message instantly
Timeout How long a rule stays in the switch before it expires and gets removed
Key Takeaway

This hands-on lab demonstrates the core SDN tradeoff: reactive flow installation adds latency for the first packet of each new flow (controller round-trip ~50ms), but all subsequent packets forward at wire-speed using installed rules. The key design decisions are: timeout tuning (short = more controller load, long = stale rules), priority assignment (security rules > routing rules), and flow table management (eviction policies when tables fill up).

132.16 What’s Next

If you want to… Read this
Study OpenFlow core concepts OpenFlow Core Concepts
Learn OpenFlow architecture in depth OpenFlow Architecture
Explore the OpenFlow protocol OpenFlow Protocol
Study SDN analytics implementations SDN Analytics and Implementations
Review SDN production deployment SDN Production Framework