%% fig-alt: OpenFlow match-action pipeline showing packet arriving at switch, being matched against flow table entries based on header fields like source IP, destination IP, protocol, and ports, then executing actions like forward, drop, modify, or queue based on the highest priority matching rule.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#FFFFFF', 'primaryBorderColor': '#16A085', 'lineColor': '#7F8C8D', 'secondaryColor': '#ECF0F1', 'tertiaryColor': '#FFFFFF'}}}%%
flowchart LR
subgraph Packet["Incoming Packet"]
P[Headers + Payload]
end
subgraph Match["Match Fields"]
M1[Source IP]
M2[Dest IP]
M3[Protocol]
M4[Ports]
M5[VLAN]
end
subgraph FlowTable["Flow Table"]
FT[Priority-ordered Rules]
end
subgraph Actions["Actions"]
A1[Forward]
A2[Drop]
A3[Modify]
A4[Queue]
end
P --> Match
Match --> FlowTable
FlowTable --> Actions
302 SDN Flow Rule Builder
Design Software-Defined Network Rules
302.1 Software-Defined Networking Flow Rules
Software-Defined Networking (SDN) separates the control plane from the data plane, allowing centralized management of network traffic through programmable flow rules. This interactive tool demonstrates how flow rules are composed, installed on switches, and affect packet forwarding.
This interactive SDN flow rule builder lets you design a network topology, compose OpenFlow-style rules, visualize rule propagation, and simulate traffic patterns. Learn how SDN enables flexible, policy-driven networking.
- View the Network Topology with 6 switches and 10 hosts
- Use the Flow Rule Composer to create match/action rules
- Click Install Rule to visualize rule propagation to switches
- Run Traffic Simulation to see packets follow the flow rules
- View Flow Tables per switch to see installed rules
- Use QoS Templates for common traffic policies
- Check Conflict Detection for overlapping rules
Show code
{
// ===========================================================================
// SDN FLOW RULE BUILDER
// ===========================================================================
// Features:
// - Network topology canvas (6 switches, 10 hosts)
// - Flow rule composer with match fields and actions
// - Rule installation animation
// - Per-switch flow table view
// - Traffic simulation with packet animation
// - QoS policy templates
// - Conflict detection
// - OpenFlow message format display
//
// IEEE Color Palette:
// Navy: #2C3E50 (primary, switches)
// Teal: #16A085 (secondary, success)
// Orange: #E67E22 (highlights, packets)
// Gray: #7F8C8D (neutral, inactive)
// LtGray: #ECF0F1 (backgrounds)
// Green: #27AE60 (allow rules)
// Red: #E74C3C (drop rules)
// Purple: #9B59B6 (QoS)
// Blue: #3498DB (hosts)
// ===========================================================================
// ---------------------------------------------------------------------------
// CONFIGURATION
// ---------------------------------------------------------------------------
const config = {
width: 950,
height: 2400,
topoWidth: 900,
topoHeight: 450,
colors: {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
green: "#27AE60",
red: "#E74C3C",
yellow: "#F1C40F",
purple: "#9B59B6",
blue: "#3498DB",
pink: "#E91E63"
},
// Network topology
switches: [
{ id: "S1", x: 150, y: 100, type: "switch" },
{ id: "S2", x: 450, y: 100, type: "switch" },
{ id: "S3", x: 750, y: 100, type: "switch" },
{ id: "S4", x: 150, y: 300, type: "switch" },
{ id: "S5", x: 450, y: 300, type: "switch" },
{ id: "S6", x: 750, y: 300, type: "switch" }
],
hosts: [
{ id: "H1", ip: "10.0.1.1", mac: "00:00:00:00:01:01", x: 50, y: 50, connectedTo: "S1", port: 1 },
{ id: "H2", ip: "10.0.1.2", mac: "00:00:00:00:01:02", x: 50, y: 150, connectedTo: "S1", port: 2 },
{ id: "H3", ip: "10.0.2.1", mac: "00:00:00:00:02:01", x: 450, y: 50, connectedTo: "S2", port: 1 },
{ id: "H4", ip: "10.0.2.2", mac: "00:00:00:00:02:02", x: 550, y: 50, connectedTo: "S2", port: 2 },
{ id: "H5", ip: "10.0.3.1", mac: "00:00:00:00:03:01", x: 850, y: 50, connectedTo: "S3", port: 1 },
{ id: "H6", ip: "10.0.3.2", mac: "00:00:00:00:03:02", x: 850, y: 150, connectedTo: "S3", port: 2 },
{ id: "H7", ip: "10.0.4.1", mac: "00:00:00:00:04:01", x: 50, y: 350, connectedTo: "S4", port: 1 },
{ id: "H8", ip: "10.0.5.1", mac: "00:00:00:00:05:01", x: 450, y: 400, connectedTo: "S5", port: 1 },
{ id: "H9", ip: "10.0.6.1", mac: "00:00:00:00:06:01", x: 850, y: 300, connectedTo: "S6", port: 1 },
{ id: "H10", ip: "10.0.6.2", mac: "00:00:00:00:06:02", x: 850, y: 400, connectedTo: "S6", port: 2 }
],
// Switch-to-switch links
links: [
{ from: "S1", to: "S2", fromPort: 3, toPort: 3 },
{ from: "S2", to: "S3", fromPort: 4, toPort: 3 },
{ from: "S1", to: "S4", fromPort: 4, toPort: 3 },
{ from: "S2", to: "S5", fromPort: 5, toPort: 3 },
{ from: "S3", to: "S6", fromPort: 4, toPort: 3 },
{ from: "S4", to: "S5", fromPort: 4, toPort: 4 },
{ from: "S5", to: "S6", fromPort: 5, toPort: 4 },
{ from: "S1", to: "S5", fromPort: 5, toPort: 6 },
{ from: "S2", to: "S6", fromPort: 6, toPort: 5 }
],
qosTemplates: [
{
name: "Voice Traffic (DSCP EF)",
description: "Low latency, high priority for VoIP",
match: { protocol: "UDP", dstPort: "5060" },
action: "queue",
queue: 0,
priority: 60000
},
{
name: "Video Streaming (DSCP AF41)",
description: "High bandwidth, assured forwarding",
match: { protocol: "TCP", dstPort: "443" },
action: "queue",
queue: 1,
priority: 50000
},
{
name: "Best Effort Data",
description: "Default traffic handling",
match: { protocol: "any" },
action: "forward",
priority: 10000
},
{
name: "Block ICMP",
description: "Security policy - drop ping",
match: { protocol: "ICMP" },
action: "drop",
priority: 55000
}
]
};
// ---------------------------------------------------------------------------
// STATE MANAGEMENT
// ---------------------------------------------------------------------------
let state = {
flowTables: {}, // Per-switch flow tables
rules: [], // All installed rules
ruleCounter: 0,
selectedSwitch: null,
isAnimating: false,
simulationRunning: false,
packets: [],
conflicts: [],
// Current rule being composed
currentRule: {
srcIP: "",
dstIP: "",
srcPort: "",
dstPort: "",
protocol: "any",
vlan: "",
action: "forward",
outPort: "",
priority: 32768,
modifyHeaders: false,
newSrcIP: "",
newDstIP: ""
}
};
// Initialize flow tables for all switches
config.switches.forEach(sw => {
state.flowTables[sw.id] = [];
});
// ---------------------------------------------------------------------------
// HELPER FUNCTIONS
// ---------------------------------------------------------------------------
function getSwitchById(id) {
return config.switches.find(s => s.id === id);
}
function getHostById(id) {
return config.hosts.find(h => h.id === id);
}
function getPath(srcSwitch, dstSwitch) {
// Simple BFS for shortest path
if (srcSwitch === dstSwitch) return [srcSwitch];
const visited = new Set();
const queue = [[srcSwitch]];
visited.add(srcSwitch);
while (queue.length > 0) {
const path = queue.shift();
const current = path[path.length - 1];
for (const link of config.links) {
let neighbor = null;
if (link.from === current) neighbor = link.to;
else if (link.to === current) neighbor = link.from;
if (neighbor && !visited.has(neighbor)) {
const newPath = [...path, neighbor];
if (neighbor === dstSwitch) return newPath;
visited.add(neighbor);
queue.push(newPath);
}
}
}
return [];
}
function matchesRule(packet, rule) {
if (rule.srcIP && packet.srcIP !== rule.srcIP && !packet.srcIP.startsWith(rule.srcIP.replace("*", ""))) return false;
if (rule.dstIP && packet.dstIP !== rule.dstIP && !packet.dstIP.startsWith(rule.dstIP.replace("*", ""))) return false;
if (rule.srcPort && packet.srcPort !== rule.srcPort) return false;
if (rule.dstPort && packet.dstPort !== rule.dstPort) return false;
if (rule.protocol !== "any" && packet.protocol !== rule.protocol) return false;
if (rule.vlan && packet.vlan !== rule.vlan) return false;
return true;
}
function detectConflicts() {
const conflicts = [];
const allRules = state.rules;
for (let i = 0; i < allRules.length; i++) {
for (let j = i + 1; j < allRules.length; j++) {
const r1 = allRules[i];
const r2 = allRules[j];
// Check for overlapping match criteria
const sameSwitch = r1.switches.some(s => r2.switches.includes(s));
if (!sameSwitch) continue;
const overlap =
(!r1.srcIP || !r2.srcIP || r1.srcIP === r2.srcIP || r1.srcIP === "*" || r2.srcIP === "*") &&
(!r1.dstIP || !r2.dstIP || r1.dstIP === r2.dstIP || r1.dstIP === "*" || r2.dstIP === "*") &&
(r1.protocol === "any" || r2.protocol === "any" || r1.protocol === r2.protocol);
if (overlap && r1.action !== r2.action) {
conflicts.push({
rule1: r1,
rule2: r2,
type: r1.priority === r2.priority ? "exact-overlap" : "shadowing",
winner: r1.priority > r2.priority ? r1 : r2
});
}
}
}
state.conflicts = conflicts;
return conflicts;
}
function generateOpenFlowMessage(rule) {
return {
version: "1.3",
type: "OFPT_FLOW_MOD",
xid: Math.floor(Math.random() * 0xFFFFFFFF),
cookie: rule.id,
command: "OFPFC_ADD",
priority: rule.priority,
match: {
oxm_fields: [
rule.srcIP ? { field: "IPV4_SRC", value: rule.srcIP } : null,
rule.dstIP ? { field: "IPV4_DST", value: rule.dstIP } : null,
rule.srcPort ? { field: "TCP_SRC", value: rule.srcPort } : null,
rule.dstPort ? { field: "TCP_DST", value: rule.dstPort } : null,
rule.protocol !== "any" ? { field: "IP_PROTO", value: rule.protocol } : null,
rule.vlan ? { field: "VLAN_VID", value: rule.vlan } : null
].filter(Boolean)
},
instructions: [
{
type: rule.action === "drop" ? "OFPIT_CLEAR_ACTIONS" :
rule.action === "queue" ? "OFPIT_WRITE_ACTIONS" : "OFPIT_APPLY_ACTIONS",
actions: rule.action === "drop" ? [] : [
{ type: "OFPAT_OUTPUT", port: rule.outPort || "FLOOD" }
]
}
]
};
}
// ---------------------------------------------------------------------------
// CREATE CONTAINER
// ---------------------------------------------------------------------------
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", `${config.width}px`)
.style("margin", "0 auto");
// ---------------------------------------------------------------------------
// NETWORK TOPOLOGY PANEL
// ---------------------------------------------------------------------------
const topoPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.navy}`);
topoPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.navy};color:white;padding:4px 10px;border-radius:4px;">1</span> Network Topology`);
const topoSvg = topoPanel.append("svg")
.attr("viewBox", `0 0 ${config.topoWidth} ${config.topoHeight}`)
.attr("width", "100%")
.style("background", config.colors.lightGray)
.style("border-radius", "8px");
// Draw links
const linksG = topoSvg.append("g").attr("class", "links");
config.links.forEach(link => {
const fromSw = getSwitchById(link.from);
const toSw = getSwitchById(link.to);
linksG.append("line")
.attr("x1", fromSw.x)
.attr("y1", fromSw.y)
.attr("x2", toSw.x)
.attr("y2", toSw.y)
.attr("stroke", config.colors.gray)
.attr("stroke-width", 3)
.attr("class", `link-${link.from}-${link.to}`);
});
// Draw host connections
config.hosts.forEach(host => {
const sw = getSwitchById(host.connectedTo);
linksG.append("line")
.attr("x1", host.x)
.attr("y1", host.y)
.attr("x2", sw.x)
.attr("y2", sw.y)
.attr("stroke", config.colors.blue)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
});
// Draw switches
const switchesG = topoSvg.append("g").attr("class", "switches");
config.switches.forEach(sw => {
const g = switchesG.append("g")
.attr("transform", `translate(${sw.x}, ${sw.y})`)
.attr("class", `switch-${sw.id}`)
.style("cursor", "pointer")
.on("click", function() {
state.selectedSwitch = state.selectedSwitch === sw.id ? null : sw.id;
updateSwitchHighlight();
updateFlowTableDisplay();
});
g.append("rect")
.attr("x", -30)
.attr("y", -20)
.attr("width", 60)
.attr("height", 40)
.attr("rx", 6)
.attr("fill", config.colors.navy)
.attr("stroke", config.colors.teal)
.attr("stroke-width", 2)
.attr("class", "switch-rect");
g.append("text")
.attr("text-anchor", "middle")
.attr("y", 5)
.attr("fill", config.colors.white)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text(sw.id);
});
// Draw hosts
const hostsG = topoSvg.append("g").attr("class", "hosts");
config.hosts.forEach(host => {
const g = hostsG.append("g")
.attr("transform", `translate(${host.x}, ${host.y})`)
.style("cursor", "pointer");
g.append("circle")
.attr("r", 18)
.attr("fill", config.colors.blue)
.attr("stroke", config.colors.navy)
.attr("stroke-width", 2);
g.append("text")
.attr("text-anchor", "middle")
.attr("y", 4)
.attr("fill", config.colors.white)
.attr("font-size", "10px")
.attr("font-weight", "bold")
.text(host.id);
// Tooltip
g.append("title")
.text(`${host.id}\nIP: ${host.ip}\nMAC: ${host.mac}\nConnected to: ${host.connectedTo} port ${host.port}`);
});
// Packets layer
const packetsG = topoSvg.append("g").attr("class", "packets");
// Legend
const legend = topoSvg.append("g")
.attr("transform", `translate(10, ${config.topoHeight - 60})`);
[
{ label: "Switch", color: config.colors.navy, shape: "rect" },
{ label: "Host", color: config.colors.blue, shape: "circle" },
{ label: "Active Path", color: config.colors.orange, shape: "line" }
].forEach((item, i) => {
const lg = legend.append("g")
.attr("transform", `translate(${i * 120}, 0)`);
if (item.shape === "rect") {
lg.append("rect")
.attr("width", 20)
.attr("height", 14)
.attr("rx", 3)
.attr("fill", item.color);
} else if (item.shape === "circle") {
lg.append("circle")
.attr("cx", 10)
.attr("cy", 7)
.attr("r", 8)
.attr("fill", item.color);
} else {
lg.append("line")
.attr("x1", 0)
.attr("y1", 7)
.attr("x2", 20)
.attr("y2", 7)
.attr("stroke", item.color)
.attr("stroke-width", 3);
}
lg.append("text")
.attr("x", 28)
.attr("y", 12)
.attr("font-size", "11px")
.attr("fill", config.colors.navy)
.text(item.label);
});
function updateSwitchHighlight() {
switchesG.selectAll(".switch-rect")
.attr("stroke", config.colors.teal)
.attr("stroke-width", 2);
if (state.selectedSwitch) {
switchesG.select(`.switch-${state.selectedSwitch} .switch-rect`)
.attr("stroke", config.colors.orange)
.attr("stroke-width", 4);
}
}
// ---------------------------------------------------------------------------
// FLOW RULE COMPOSER
// ---------------------------------------------------------------------------
const composerPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.navy}`);
composerPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.navy};color:white;padding:4px 10px;border-radius:4px;">2</span> Flow Rule Composer`);
// Match Fields
const matchSection = composerPanel.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("margin-bottom", "15px");
matchSection.append("h4")
.style("margin", "0 0 12px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Match Fields");
const matchGrid = matchSection.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(150px, 1fr))")
.style("gap", "12px");
// Create input fields
function createInput(parent, label, placeholder, key, type = "text") {
const group = parent.append("div");
group.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "4px")
.style("text-transform", "uppercase")
.text(label);
const input = group.append("input")
.attr("type", type)
.attr("placeholder", placeholder)
.style("width", "100%")
.style("padding", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "4px")
.style("font-size", "12px")
.style("box-sizing", "border-box")
.on("input", function() {
state.currentRule[key] = this.value;
});
return input;
}
createInput(matchGrid, "Source IP", "10.0.1.* or any", "srcIP");
createInput(matchGrid, "Destination IP", "10.0.2.1 or any", "dstIP");
createInput(matchGrid, "Source Port", "1024-65535", "srcPort");
createInput(matchGrid, "Destination Port", "80, 443, 5060", "dstPort");
// Protocol dropdown
const protoGroup = matchGrid.append("div");
protoGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "4px")
.style("text-transform", "uppercase")
.text("Protocol");
const protoSelect = protoGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "4px")
.style("font-size", "12px")
.style("box-sizing", "border-box")
.on("change", function() {
state.currentRule.protocol = this.value;
});
["any", "TCP", "UDP", "ICMP"].forEach(proto => {
protoSelect.append("option")
.attr("value", proto)
.text(proto);
});
createInput(matchGrid, "VLAN ID", "1-4094 or empty", "vlan");
// Action Fields
const actionSection = composerPanel.append("div")
.style("background", config.colors.white)
.style("padding", "15px")
.style("border-radius", "8px")
.style("margin-bottom", "15px");
actionSection.append("h4")
.style("margin", "0 0 12px 0")
.style("color", config.colors.navy)
.style("font-size", "14px")
.text("Actions");
const actionGrid = actionSection.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(150px, 1fr))")
.style("gap", "12px");
// Action type
const actionGroup = actionGrid.append("div");
actionGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "4px")
.style("text-transform", "uppercase")
.text("Action");
const actionSelect = actionGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border", `1px solid ${config.colors.gray}`)
.style("border-radius", "4px")
.style("font-size", "12px")
.style("box-sizing", "border-box")
.on("change", function() {
state.currentRule.action = this.value;
updateActionOptions();
});
[
{ value: "forward", label: "Forward to Port" },
{ value: "drop", label: "Drop Packet" },
{ value: "modify", label: "Modify Headers" },
{ value: "queue", label: "Set QoS Queue" }
].forEach(action => {
actionSelect.append("option")
.attr("value", action.value)
.text(action.label);
});
const outPortInput = createInput(actionGrid, "Output Port", "1-6 or FLOOD", "outPort");
// Priority slider
const priorityGroup = actionGrid.append("div")
.style("grid-column", "span 2");
const priorityHeader = priorityGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("margin-bottom", "4px");
priorityHeader.append("label")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("text-transform", "uppercase")
.text("Priority");
const priorityValue = priorityHeader.append("span")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("color", config.colors.teal)
.text(state.currentRule.priority);
const prioritySlider = priorityGroup.append("input")
.attr("type", "range")
.attr("min", 0)
.attr("max", 65535)
.attr("value", state.currentRule.priority)
.style("width", "100%")
.style("cursor", "pointer")
.style("accent-color", config.colors.teal)
.on("input", function() {
state.currentRule.priority = +this.value;
priorityValue.text(this.value);
});
priorityGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("font-size", "10px")
.style("color", config.colors.gray)
.html("<span>0 (Lowest)</span><span>32768 (Default)</span><span>65535 (Highest)</span>");
function updateActionOptions() {
// Enable/disable options based on action type
outPortInput.property("disabled", state.currentRule.action === "drop");
}
// Install Button
const buttonRow = composerPanel.append("div")
.style("display", "flex")
.style("gap", "10px")
.style("margin-top", "15px");
const installBtn = buttonRow.append("button")
.style("flex", "1")
.style("padding", "12px 20px")
.style("background", config.colors.teal)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("font-size", "14px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Install Rule")
.on("click", installRule);
const clearBtn = buttonRow.append("button")
.style("padding", "12px 20px")
.style("background", config.colors.gray)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("font-size", "14px")
.style("cursor", "pointer")
.text("Clear All Rules")
.on("click", clearAllRules);
// ---------------------------------------------------------------------------
// QOS TEMPLATES
// ---------------------------------------------------------------------------
const qosPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.purple}`);
qosPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.purple};color:white;padding:4px 10px;border-radius:4px;">3</span> QoS Policy Templates`);
const qosGrid = qosPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(200px, 1fr))")
.style("gap", "12px");
config.qosTemplates.forEach(template => {
const card = qosGrid.append("div")
.style("background", config.colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("cursor", "pointer")
.style("border", `2px solid transparent`)
.style("transition", "all 0.2s")
.on("mouseenter", function() {
d3.select(this).style("border-color", config.colors.purple);
})
.on("mouseleave", function() {
d3.select(this).style("border-color", "transparent");
})
.on("click", function() {
applyQoSTemplate(template);
});
card.append("div")
.style("font-weight", "bold")
.style("color", config.colors.navy)
.style("font-size", "13px")
.style("margin-bottom", "5px")
.text(template.name);
card.append("div")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "8px")
.text(template.description);
const badge = card.append("span")
.style("display", "inline-block")
.style("padding", "3px 8px")
.style("border-radius", "12px")
.style("font-size", "10px")
.style("font-weight", "bold");
if (template.action === "drop") {
badge.style("background", config.colors.red + "20")
.style("color", config.colors.red)
.text("DROP");
} else if (template.action === "queue") {
badge.style("background", config.colors.purple + "20")
.style("color", config.colors.purple)
.text(`Queue ${template.queue}`);
} else {
badge.style("background", config.colors.green + "20")
.style("color", config.colors.green)
.text("FORWARD");
}
});
function applyQoSTemplate(template) {
state.currentRule.protocol = template.match.protocol || "any";
state.currentRule.dstPort = template.match.dstPort || "";
state.currentRule.action = template.action;
state.currentRule.priority = template.priority;
// Update UI
protoSelect.property("value", state.currentRule.protocol);
prioritySlider.property("value", state.currentRule.priority);
priorityValue.text(state.currentRule.priority);
actionSelect.property("value", state.currentRule.action);
// Flash feedback
installBtn.style("background", config.colors.orange);
setTimeout(() => installBtn.style("background", config.colors.teal), 300);
}
// ---------------------------------------------------------------------------
// FLOW TABLE DISPLAY
// ---------------------------------------------------------------------------
const flowTablePanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.navy}`);
flowTablePanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.navy};color:white;padding:4px 10px;border-radius:4px;">4</span> Flow Tables <span style="font-weight:normal;font-size:12px;color:${config.colors.gray}">(Click a switch to view)</span>`);
const flowTableContainer = flowTablePanel.append("div")
.attr("class", "flow-table-container");
function updateFlowTableDisplay() {
flowTableContainer.selectAll("*").remove();
if (!state.selectedSwitch) {
flowTableContainer.append("div")
.style("text-align", "center")
.style("padding", "30px")
.style("color", config.colors.gray)
.text("Click on a switch in the topology to view its flow table");
return;
}
const table = state.flowTables[state.selectedSwitch];
flowTableContainer.append("div")
.style("margin-bottom", "15px")
.style("padding", "10px")
.style("background", config.colors.navy + "10")
.style("border-radius", "6px")
.style("border-left", `4px solid ${config.colors.navy}`)
.html(`<strong>Switch ${state.selectedSwitch}</strong> - ${table.length} flow entries`);
if (table.length === 0) {
flowTableContainer.append("div")
.style("text-align", "center")
.style("padding", "20px")
.style("color", config.colors.gray)
.style("font-style", "italic")
.text("No flow entries installed");
return;
}
const tableEl = flowTableContainer.append("table")
.style("width", "100%")
.style("border-collapse", "collapse")
.style("font-size", "11px");
const header = tableEl.append("thead").append("tr")
.style("background", config.colors.navy)
.style("color", config.colors.white);
["Priority", "Match", "Action", "Packets", "Bytes"].forEach(col => {
header.append("th")
.style("padding", "10px")
.style("text-align", "left")
.text(col);
});
const tbody = tableEl.append("tbody");
table.sort((a, b) => b.priority - a.priority).forEach((entry, idx) => {
const row = tbody.append("tr")
.style("background", idx % 2 === 0 ? config.colors.white : config.colors.lightGray);
row.append("td")
.style("padding", "8px")
.style("font-weight", "bold")
.style("color", config.colors.teal)
.text(entry.priority);
// Match criteria
const matchParts = [];
if (entry.srcIP) matchParts.push(`src=${entry.srcIP}`);
if (entry.dstIP) matchParts.push(`dst=${entry.dstIP}`);
if (entry.protocol !== "any") matchParts.push(`proto=${entry.protocol}`);
if (entry.dstPort) matchParts.push(`dport=${entry.dstPort}`);
row.append("td")
.style("padding", "8px")
.style("font-family", "monospace")
.style("font-size", "10px")
.text(matchParts.length > 0 ? matchParts.join(", ") : "*");
// Action
const actionCell = row.append("td")
.style("padding", "8px");
const actionBadge = actionCell.append("span")
.style("padding", "3px 8px")
.style("border-radius", "10px")
.style("font-size", "10px")
.style("font-weight", "bold");
if (entry.action === "drop") {
actionBadge.style("background", config.colors.red + "20")
.style("color", config.colors.red)
.text("DROP");
} else if (entry.action === "queue") {
actionBadge.style("background", config.colors.purple + "20")
.style("color", config.colors.purple)
.text(`Queue ${entry.queue || 0}`);
} else {
actionBadge.style("background", config.colors.green + "20")
.style("color", config.colors.green)
.text(`OUT:${entry.outPort || "FLOOD"}`);
}
row.append("td")
.style("padding", "8px")
.style("color", config.colors.navy)
.text(entry.packetCount || 0);
row.append("td")
.style("padding", "8px")
.style("color", config.colors.navy)
.text(`${entry.byteCount || 0} B`);
});
}
// ---------------------------------------------------------------------------
// TRAFFIC SIMULATION
// ---------------------------------------------------------------------------
const simPanel = container.append("div")
.style("background", config.colors.lightGray)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.orange}`);
simPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.orange};color:white;padding:4px 10px;border-radius:4px;">5</span> Traffic Simulation`);
const simControls = simPanel.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(auto-fit, minmax(150px, 1fr))")
.style("gap", "12px")
.style("margin-bottom", "15px");
// Source host selector
const srcHostGroup = simControls.append("div");
srcHostGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "4px")
.text("Source Host");
const srcHostSelect = srcHostGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${config.colors.gray}`);
config.hosts.forEach(host => {
srcHostSelect.append("option")
.attr("value", host.id)
.text(`${host.id} (${host.ip})`);
});
// Destination host selector
const dstHostGroup = simControls.append("div");
dstHostGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "4px")
.text("Destination Host");
const dstHostSelect = dstHostGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${config.colors.gray}`);
config.hosts.forEach(host => {
dstHostSelect.append("option")
.attr("value", host.id)
.text(`${host.id} (${host.ip})`);
});
dstHostSelect.property("value", "H5");
// Protocol selector for simulation
const simProtoGroup = simControls.append("div");
simProtoGroup.append("label")
.style("display", "block")
.style("font-size", "11px")
.style("color", config.colors.gray)
.style("margin-bottom", "4px")
.text("Protocol");
const simProtoSelect = simProtoGroup.append("select")
.style("width", "100%")
.style("padding", "8px")
.style("border-radius", "4px")
.style("border", `1px solid ${config.colors.gray}`);
["TCP", "UDP", "ICMP"].forEach(proto => {
simProtoSelect.append("option")
.attr("value", proto)
.text(proto);
});
// Simulation buttons
const simBtnGroup = simControls.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "8px");
const sendPacketBtn = simBtnGroup.append("button")
.style("padding", "10px")
.style("background", config.colors.orange)
.style("color", config.colors.white)
.style("border", "none")
.style("border-radius", "6px")
.style("font-size", "12px")
.style("font-weight", "bold")
.style("cursor", "pointer")
.text("Send Packet")
.on("click", sendPacket);
const simStatusDiv = simPanel.append("div")
.attr("class", "sim-status")
.style("padding", "15px")
.style("background", config.colors.white)
.style("border-radius", "8px")
.style("min-height", "60px");
simStatusDiv.append("div")
.style("color", config.colors.gray)
.style("font-size", "12px")
.text("Click 'Send Packet' to simulate traffic flow");
// ---------------------------------------------------------------------------
// CONFLICT DETECTION
// ---------------------------------------------------------------------------
const conflictPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("margin-bottom", "15px")
.style("border", `2px solid ${config.colors.red}`);
conflictPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.red};color:white;padding:4px 10px;border-radius:4px;">6</span> Rule Conflict Detection`);
const conflictContainer = conflictPanel.append("div")
.attr("class", "conflict-container");
function updateConflictDisplay() {
conflictContainer.selectAll("*").remove();
const conflicts = detectConflicts();
if (conflicts.length === 0) {
conflictContainer.append("div")
.style("padding", "20px")
.style("text-align", "center")
.style("background", config.colors.green + "10")
.style("border-radius", "8px")
.style("color", config.colors.green)
.html(`<strong>✓ No conflicts detected</strong><br><span style="font-size:12px">All rules have distinct match criteria or consistent actions</span>`);
return;
}
conflictContainer.append("div")
.style("padding", "10px")
.style("background", config.colors.red + "10")
.style("border-radius", "6px")
.style("margin-bottom", "15px")
.style("color", config.colors.red)
.html(`<strong>⚠ ${conflicts.length} conflict(s) detected</strong>`);
conflicts.forEach((conflict, idx) => {
const card = conflictContainer.append("div")
.style("padding", "15px")
.style("background", config.colors.lightGray)
.style("border-radius", "8px")
.style("margin-bottom", "10px")
.style("border-left", `4px solid ${config.colors.red}`);
card.append("div")
.style("font-weight", "bold")
.style("color", config.colors.navy)
.style("margin-bottom", "8px")
.text(`Conflict #${idx + 1}: ${conflict.type === "exact-overlap" ? "Exact Overlap" : "Rule Shadowing"}`);
const ruleDisplay = (rule, label) => {
return `<div style="margin-bottom:5px"><strong>${label}:</strong>
Match: ${rule.srcIP || "*"} → ${rule.dstIP || "*"} |
Action: ${rule.action} |
Priority: ${rule.priority}</div>`;
};
card.append("div")
.style("font-size", "12px")
.style("font-family", "monospace")
.html(ruleDisplay(conflict.rule1, "Rule A") + ruleDisplay(conflict.rule2, "Rule B"));
card.append("div")
.style("margin-top", "8px")
.style("font-size", "11px")
.style("color", config.colors.teal)
.html(`<strong>Resolution:</strong> Rule with priority ${conflict.winner.priority} takes precedence`);
});
}
// ---------------------------------------------------------------------------
// OPENFLOW MESSAGE DISPLAY
// ---------------------------------------------------------------------------
const ofPanel = container.append("div")
.style("background", config.colors.white)
.style("border-radius", "12px")
.style("padding", "20px")
.style("border", `2px solid ${config.colors.navy}`);
ofPanel.append("h3")
.style("margin", "0 0 15px 0")
.style("color", config.colors.navy)
.style("font-size", "18px")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.html(`<span style="background:${config.colors.navy};color:white;padding:4px 10px;border-radius:4px;">7</span> OpenFlow Message Format`);
const ofContainer = ofPanel.append("div")
.attr("class", "of-container");
ofContainer.append("div")
.style("padding", "20px")
.style("text-align", "center")
.style("color", config.colors.gray)
.text("Install a rule to see the OpenFlow message format");
function updateOpenFlowDisplay(rule) {
ofContainer.selectAll("*").remove();
const msg = generateOpenFlowMessage(rule);
ofContainer.append("div")
.style("margin-bottom", "10px")
.style("font-size", "12px")
.style("color", config.colors.gray)
.text("OpenFlow 1.3 FLOW_MOD Message:");
const pre = ofContainer.append("pre")
.style("background", config.colors.navy)
.style("color", config.colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("font-size", "11px")
.style("overflow-x", "auto")
.style("white-space", "pre-wrap")
.text(JSON.stringify(msg, null, 2));
}
// ---------------------------------------------------------------------------
// CORE FUNCTIONS
// ---------------------------------------------------------------------------
function installRule() {
if (state.isAnimating) return;
const rule = { ...state.currentRule, id: ++state.ruleCounter };
// Determine which switches need this rule
// For simplicity, install on all switches in the path between any matching hosts
const affectedSwitches = new Set();
// If no specific IPs, install on all switches
if (!rule.srcIP && !rule.dstIP) {
config.switches.forEach(sw => affectedSwitches.add(sw.id));
} else {
// Find hosts matching the rule
const srcHosts = config.hosts.filter(h =>
!rule.srcIP || h.ip === rule.srcIP || h.ip.startsWith(rule.srcIP.replace("*", ""))
);
const dstHosts = config.hosts.filter(h =>
!rule.dstIP || h.ip === rule.dstIP || h.ip.startsWith(rule.dstIP.replace("*", ""))
);
srcHosts.forEach(srcHost => {
dstHosts.forEach(dstHost => {
const path = getPath(srcHost.connectedTo, dstHost.connectedTo);
path.forEach(sw => affectedSwitches.add(sw));
});
});
}
rule.switches = Array.from(affectedSwitches);
// Animate rule propagation
state.isAnimating = true;
animateRuleInstall(rule, Array.from(affectedSwitches), () => {
// Add to flow tables
rule.switches.forEach(swId => {
state.flowTables[swId].push({
...rule,
packetCount: 0,
byteCount: 0
});
});
state.rules.push(rule);
state.isAnimating = false;
updateFlowTableDisplay();
updateConflictDisplay();
updateOpenFlowDisplay(rule);
});
}
function animateRuleInstall(rule, switches, callback) {
let index = 0;
function animateNext() {
if (index >= switches.length) {
callback();
return;
}
const swId = switches[index];
const sw = getSwitchById(swId);
// Highlight switch
const swRect = switchesG.select(`.switch-${swId} .switch-rect`);
swRect.attr("fill", config.colors.teal);
// Create propagation circle
const circle = topoSvg.append("circle")
.attr("cx", sw.x)
.attr("cy", sw.y)
.attr("r", 5)
.attr("fill", config.colors.orange)
.attr("opacity", 1);
circle.transition()
.duration(500)
.attr("r", 40)
.attr("opacity", 0)
.remove();
setTimeout(() => {
swRect.attr("fill", config.colors.navy);
index++;
animateNext();
}, 300);
}
animateNext();
}
function clearAllRules() {
config.switches.forEach(sw => {
state.flowTables[sw.id] = [];
});
state.rules = [];
state.conflicts = [];
updateFlowTableDisplay();
updateConflictDisplay();
ofContainer.selectAll("*").remove();
ofContainer.append("div")
.style("padding", "20px")
.style("text-align", "center")
.style("color", config.colors.gray)
.text("All rules cleared");
}
function sendPacket() {
const srcHost = getHostById(srcHostSelect.property("value"));
const dstHost = getHostById(dstHostSelect.property("value"));
const protocol = simProtoSelect.property("value");
if (srcHost.id === dstHost.id) {
simStatusDiv.selectAll("*").remove();
simStatusDiv.append("div")
.style("color", config.colors.red)
.text("Source and destination cannot be the same");
return;
}
const packet = {
srcIP: srcHost.ip,
dstIP: dstHost.ip,
srcPort: Math.floor(Math.random() * 60000 + 1024),
dstPort: protocol === "TCP" ? 80 : protocol === "UDP" ? 53 : 0,
protocol: protocol
};
const path = [srcHost.connectedTo, ...getPath(srcHost.connectedTo, dstHost.connectedTo).slice(1)];
// Check if packet would be dropped
let dropped = false;
let dropSwitch = null;
for (const swId of path) {
const table = state.flowTables[swId];
const matchingRules = table.filter(r => matchesRule(packet, r))
.sort((a, b) => b.priority - a.priority);
if (matchingRules.length > 0 && matchingRules[0].action === "drop") {
dropped = true;
dropSwitch = swId;
break;
}
}
// Animate packet
animatePacket(srcHost, dstHost, path, dropped, dropSwitch, packet);
}
function animatePacket(srcHost, dstHost, path, dropped, dropSwitch, packet) {
simStatusDiv.selectAll("*").remove();
const statusText = simStatusDiv.append("div")
.style("font-size", "12px")
.style("color", config.colors.navy);
statusText.append("div")
.style("margin-bottom", "8px")
.html(`<strong>Packet:</strong> ${packet.srcIP} → ${packet.dstIP} (${packet.protocol})`);
const pathStr = path.join(" → ");
statusText.append("div")
.style("font-family", "monospace")
.style("font-size", "11px")
.html(`<strong>Path:</strong> ${srcHost.id} → ${pathStr} → ${dstHost.id}`);
// Create packet visual
const packetVisual = packetsG.append("circle")
.attr("cx", srcHost.x)
.attr("cy", srcHost.y)
.attr("r", 8)
.attr("fill", config.colors.orange)
.attr("stroke", config.colors.navy)
.attr("stroke-width", 2);
// Build waypoints
const waypoints = [{ x: srcHost.x, y: srcHost.y }];
path.forEach(swId => {
const sw = getSwitchById(swId);
waypoints.push({ x: sw.x, y: sw.y });
});
if (!dropped) {
waypoints.push({ x: dstHost.x, y: dstHost.y });
}
// Animate through waypoints
let wpIndex = 0;
function animateToNext() {
if (wpIndex >= waypoints.length - 1) {
if (dropped) {
// Drop animation
packetVisual.transition()
.duration(300)
.attr("r", 0)
.attr("fill", config.colors.red)
.remove();
statusText.append("div")
.style("margin-top", "8px")
.style("color", config.colors.red)
.style("font-weight", "bold")
.text(`✗ Packet DROPPED at ${dropSwitch}`);
} else {
// Success
packetVisual.transition()
.duration(300)
.attr("r", 15)
.attr("opacity", 0)
.remove();
statusText.append("div")
.style("margin-top", "8px")
.style("color", config.colors.green)
.style("font-weight", "bold")
.text(`✓ Packet delivered successfully`);
// Update packet counts
path.forEach(swId => {
const table = state.flowTables[swId];
const matchingRules = table.filter(r => matchesRule(packet, r));
matchingRules.forEach(r => {
r.packetCount = (r.packetCount || 0) + 1;
r.byteCount = (r.byteCount || 0) + 64;
});
});
if (state.selectedSwitch) {
updateFlowTableDisplay();
}
}
return;
}
const nextWp = waypoints[wpIndex + 1];
// Highlight path segment
if (wpIndex > 0 && wpIndex < path.length) {
const fromSw = path[wpIndex - 1];
const toSw = path[wpIndex];
linksG.select(`.link-${fromSw}-${toSw}, .link-${toSw}-${fromSw}`)
.attr("stroke", config.colors.orange)
.attr("stroke-width", 5);
}
packetVisual.transition()
.duration(400)
.attr("cx", nextWp.x)
.attr("cy", nextWp.y)
.on("end", () => {
wpIndex++;
animateToNext();
});
}
animateToNext();
// Reset link colors after animation
setTimeout(() => {
linksG.selectAll("line")
.attr("stroke", config.colors.gray)
.attr("stroke-width", 3);
}, path.length * 500 + 1000);
}
// ---------------------------------------------------------------------------
// INITIALIZE
// ---------------------------------------------------------------------------
updateFlowTableDisplay();
updateConflictDisplay();
return container.node();
}302.2 Understanding SDN Flow Rules
302.2.1 OpenFlow Match/Action Model
SDN uses a match/action paradigm where packets are classified by matching header fields and then processed according to defined actions:
%% fig-alt: Decision tree showing how SDN switches process incoming packets through priority-ordered flow rule matching, with decision points for each match field and outcomes leading to specific actions or table-miss handling.
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#FFFFFF', 'primaryBorderColor': '#16A085', 'lineColor': '#7F8C8D', 'secondaryColor': '#ECF0F1', 'tertiaryColor': '#FFFFFF'}}}%%
flowchart TD
PKT[Packet Arrives] --> RULE1{Rule 1<br/>Priority: 65535<br/>Match: ICMP}
RULE1 -->|Match| DROP[DROP<br/>Block ping]
RULE1 -->|No Match| RULE2{Rule 2<br/>Priority: 50000<br/>Match: VoIP Port 5060}
RULE2 -->|Match| QUEUE[QUEUE 0<br/>High priority]
RULE2 -->|No Match| RULE3{Rule 3<br/>Priority: 32768<br/>Match: Dst 10.0.2.x}
RULE3 -->|Match| FWD[FORWARD<br/>Port 3]
RULE3 -->|No Match| DEFAULT{Default Rule<br/>Priority: 0<br/>Match: ANY}
DEFAULT -->|Match| CTRL[PACKET-IN<br/>Send to Controller]
style PKT fill:#2C3E50,stroke:#16A085,stroke-width:2px,color:#fff
style DROP fill:#E74C3C,stroke:#2C3E50,stroke-width:2px,color:#fff
style QUEUE fill:#9B59B6,stroke:#2C3E50,stroke-width:2px,color:#fff
style FWD fill:#27AE60,stroke:#2C3E50,stroke-width:2px,color:#fff
style CTRL fill:#E67E22,stroke:#2C3E50,stroke-width:2px,color:#fff
style RULE1 fill:#7F8C8D,stroke:#2C3E50,stroke-width:1px,color:#fff
style RULE2 fill:#7F8C8D,stroke:#2C3E50,stroke-width:1px,color:#fff
style RULE3 fill:#7F8C8D,stroke:#2C3E50,stroke-width:1px,color:#fff
style DEFAULT fill:#7F8C8D,stroke:#2C3E50,stroke-width:1px,color:#fff
This decision tree shows how packets traverse the flow table from highest to lowest priority. Each rule is evaluated in sequence until a match is found, demonstrating the “first-match-wins” behavior. If no rules match, the default table-miss action sends the packet to the controller for handling.
302.2.2 Flow Rule Components
| Component | Description | Example Values |
|---|---|---|
| Match Fields | Packet header fields to match | IP addresses, ports, protocol |
| Priority | Rule precedence (higher = first) | 0-65535 |
| Actions | What to do with matched packets | Forward, drop, modify |
| Counters | Statistics for matched packets | Packet/byte counts |
| Timeouts | Rule expiration | Idle/hard timeouts |
302.2.3 QoS with SDN
SDN enables sophisticated Quality of Service through:
- Traffic Classification: Match specific flows (VoIP, video, etc.)
- Queue Assignment: Direct traffic to appropriate queues
- Rate Limiting: Meter actions for bandwidth control
- Priority Marking: Set DSCP/CoS values
302.2.4 Common Flow Rule Patterns
%% fig-alt: "Common SDN flow rule patterns showing four categories - basic forwarding rules that match destination and output to port, security rules that match threats and drop packets, QoS rules that match traffic type and assign queue, and load balancing rules that match service and distribute across servers."
%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2C3E50', 'primaryTextColor': '#FFFFFF', 'primaryBorderColor': '#16A085', 'lineColor': '#7F8C8D', 'secondaryColor': '#ECF0F1', 'tertiaryColor': '#FFFFFF'}}}%%
flowchart TB
subgraph Basic["Basic Forwarding"]
B1["Match: dst_ip=10.0.2.0/24<br/>Action: output:port3"]
end
subgraph Security["Security Policy"]
S1["Match: proto=ICMP<br/>Action: DROP"]
end
subgraph QoS["QoS Policy"]
Q1["Match: dst_port=5060<br/>Action: set_queue:0"]
end
subgraph LB["Load Balancing"]
L1["Match: dst_ip=VIP<br/>Action: set_dst:Server1"]
end
302.3 What’s Next
Explore related networking and architecture topics:
- SDN Fundamentals and OpenFlow - Control plane separation and protocol details
- SDN IoT Variants and Challenges - SDN in IoT environments
- SDN Controller Basics - Understanding SDN controllers
- Network Security - Securing SDN deployments
Interactive tool created for the IoT Class Textbook - SDN-FLOW-001