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
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
- SDN Core Concepts: Understanding of control/data plane separation
- SDN Architecture: Knowledge of three-layer architecture
- OpenFlow Protocol: Understanding of flow tables and OpenFlow messages
287.3 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.
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):
- First Packet -> Table miss (Yellow LED) -> Controller installs rule (Blue LED) -> Rule confirmed (Green LED)
- Subsequent Packets -> Table hit -> Fast hardware forwarding (Quick green blink, no controller involvement)
- Flow Expiration -> Red LED blinks when idle/hard timeout expires
- 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:
- Reactive Flow Installation: First packet triggers controller (yellow -> blue -> green sequence), subsequent packets use fast path (quick green only)
- Priority Behavior: High-priority flows (press Priority button) get priority=1000 and dedicated port 1, normal flows use priority=100 and round-robin ports
- Timeout Management: With potentiometer at minimum (5s), flows expire quickly causing more PACKET_IN messages. At maximum (60s), flows persist longer reducing controller load
- Statistics Growth: Every 10 seconds, statistics show increasing PACKET_IN and FLOW_MOD counts, tracking flow lifecycle
- Flow Table State: Active flows display shows packet counts increasing for matched flows, idle time resetting on each match
- 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:
Centralized Control, Distributed Forwarding: Controller makes smart decisions, switches execute them fast. Best of both worlds.
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.
Priority Matters: High-priority rules (security, QoS) must have higher priority values than general routing rules. Order determines which rule wins when multiple match.
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.
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.
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.