287  SDN Controller Hands-On Lab

287.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
  • Handle PACKET_IN Messages: Process unknown packets and make forwarding decisions
  • Install Flow Rules: Use FLOW_MOD-style messages to program switch behavior
  • Manage Flow Timeouts: Implement idle and hard timeouts for automatic flow expiration
  • Track Flow Statistics: Monitor packet counts, byte counts, and flow lifetimes
TipMVU: 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).

287.2 Prerequisites


287.3 Lab Overview

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

NoteLab 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.


287.4 Interactive Simulation


287.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"]]
  ]
}

287.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);
}

287.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)

287.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.


287.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

287.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.

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

  4. 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.

  5. 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.

  6. 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).


287.11 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

287.12 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: - SDN Analytics - Anomaly detection with SDN - SDN Production - Real-world controller deployment

Comparisons: - Traditional Networking - Distributed vs centralized - MANET vs SDN - Mobile ad-hoc networks

Applications: - IoT Network Design - SDN for IoT

Learning: - Quizzes Hub - OpenFlow protocol exercises - Videos Hub - SDN controller tutorials

287.13 What’s Next

The next chapter examines UAV Networks: Fundamentals and Topologies, exploring how SDN principles apply to three-dimensional mobile networks of unmanned aerial vehicles used for aerial sensing, communication relay, and emergency coverage.

Continue to UAV Networks →