net/go.book
All Parts Marketing

SDN (Software Defined Networking) and OpenFlow with Go

"A city where every traffic light decides on its own, with no coordination, when to turn green is a city with a lot of avoidable gridlock. A city with one control center watching every intersection and adjusting all the lights together can clear traffic that no single light could reason about alone. Traditional networking is the first city: every switch and router makes its own forwarding decisions from local information. Software Defined Networking is the second: a central controller sees the whole picture and tells the switches what to do."

Every device you have programmed against so far in this book -- a TCP listener, an HTTP server -- has been an endpoint. SDN asks a different question: what if the switches and routers in between were programmable too? Traditional network gear bundles two jobs into one box: the data plane (forwarding packets, one hop at a time, as fast as silicon allows) and the control plane (deciding how to forward -- routing tables, spanning tree, ACLs). SDN's central idea is to rip those apart: keep dumb, fast switches for the data plane, and move all the decision-making into software running on general-purpose servers, talking to the switches over a well-defined protocol.

OpenFlow: The Protocol That Made This Concrete

OpenFlow, the protocol that popularized SDN, defines exactly that control channel. An OpenFlow switch maintains one or more flow tables: ordered lists of match-action rules. A rule matches on packet header fields (input port, Ethernet type, IP addresses, TCP ports, VLAN tags) and specifies an action (forward out a port, drop, modify a header, send to the controller). When a packet arrives that matches no existing rule, the switch can encapsulate it and forward it to the controller, which decides what to do and can install a new flow rule so future packets of that type are handled locally, without another round trip.

The controller-switch conversation follows a defined sequence: both sides exchange OFPT_HELLO messages to agree on a protocol version, the controller sends a Features Request and gets back the switch's capabilities and port list, and from then on the controller pushes Flow-Mod messages to install, modify, or delete flow entries.

Go Implementation: A Minimal Controller Skeleton

Rather than fabricate an OpenFlow binary encoder (getting the wire format subtly wrong would be worse than not showing it), this example builds the part of an SDN controller that is both realistic and safe to get right in Go: a northbound HTTP API that operators or higher-level systems call to express intent ("route this traffic this way"), backed by an in-memory flow table, plus a southbound TCP listener where switch agents connect and receive flow updates as simple JSON messages. This mirrors the real ONOS/OpenDaylight design -- REST north, a switch protocol south -- using a deliberately simplified southbound message format instead of raw OpenFlow framing.

package main

import (
	"encoding/json"
	"log"
	"net"
	"net/http"
	"sync"
)

// FlowRule is our simplified match-action rule, analogous in spirit
// to an OpenFlow flow entry, but encoded as plain JSON instead of
// OpenFlow's binary wire format.
type FlowRule struct {
	SwitchID string `json:"switch_id"`
	MatchSrc string `json:"match_src"`
	MatchDst string `json:"match_dst"`
	Action   string `json:"action"` // "forward:<port>" or "drop"
}

type Controller struct {
	mu       sync.Mutex
	flows    []FlowRule
	switches map[string]net.Conn
}

func NewController() *Controller {
	return &Controller{switches: make(map[string]net.Conn)}
}

// handleInstallFlow is the northbound REST endpoint: an operator or
// orchestration system posts intent here.
func (c *Controller) handleInstallFlow(w http.ResponseWriter, r *http.Request) {
	var rule FlowRule
	if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	c.mu.Lock()
	c.flows = append(c.flows, rule)
	conn, connected := c.switches[rule.SwitchID]
	c.mu.Unlock()

	if !connected {
		http.Error(w, "switch not connected", http.StatusNotFound)
		return
	}

	if err := json.NewEncoder(conn).Encode(rule); err != nil {
		http.Error(w, "failed to push rule: "+err.Error(),
			http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusAccepted)
}

// serveSouthbound accepts switch-agent connections and registers them
// by the ID they announce, so the controller can push flow updates.
func (c *Controller) serveSouthbound(addr string) error {
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	log.Println("southbound channel listening on", addr)

	for {
		conn, err := ln.Accept()
		if err != nil {
			continue
		}
		go c.registerSwitch(conn)
	}
}

func (c *Controller) registerSwitch(conn net.Conn) {
	var hello struct {
		SwitchID string `json:"switch_id"`
	}
	if err := json.NewDecoder(conn).Decode(&hello); err != nil {
		conn.Close()
		return
	}
	c.mu.Lock()
	c.switches[hello.SwitchID] = conn
	c.mu.Unlock()
	log.Println("switch registered:", hello.SwitchID)
}

func main() {
	controller := NewController()
	go controller.serveSouthbound(":6653") // 6653 is OpenFlow's assigned port

	http.HandleFunc("/flows", controller.handleInstallFlow)
	log.Println("northbound REST API listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

A switch-side agent would dial :6653, send its switch_id as a JSON hello, and then sit reading a stream of FlowRule JSON objects, translating each into whatever its actual forwarding mechanism supports (an eBPF program, iptables rules, or a real OpenFlow-speaking switch behind a translation layer). The controller never needs to know that detail -- it only deals in intent.

This is a teaching model, not a drop-in OpenFlow replacement
If your target switches genuinely speak OpenFlow (Open vSwitch, most SDN-capable hardware), do not hand-roll the wire protocol in production. Either drive Open vSwitch through its OVSDB management protocol (JSON-RPC over TCP, which is straightforward to speak from Go with encoding/json and net.Dial) or use an existing controller (ONOS, OpenDaylight, Ryu) and have your Go service talk to its REST API instead of the switches directly.

Why This Model Still Matters

Even where Go is not driving raw OpenFlow frames, the SDN pattern -- centralize decisions, keep the data plane dumb and fast, expose a programmable API to the control logic -- shows up everywhere in modern infrastructure: Kubernetes' networking plugins, service meshes, and cloud provider virtual networks are all, in effect, software-defined networks with a controller making global decisions and lightweight agents enforcing them locally. Understanding the controller/agent split here pays off directly in the Zero Trust and NFV chapters that follow.

Frequently Asked Questions

Is the JSON-based FlowRule controller in this chapter actually speaking OpenFlow? No, and the DeepDive and Warning boxes both say so directly -- it is a teaching model that mirrors OpenFlow's shape (a northbound API, a southbound channel, match-action rules) without claiming to be a byte-exact implementation of the wire protocol. If your real switches speak OpenFlow, drive them through Open vSwitch's OVSDB management protocol or an existing controller like ONOS or OpenDaylight instead of hand-rolling the frame encoding.

Why doesn't this chapter just show a real OpenFlow encoder in Go? Because getting a binary protocol like OpenFlow subtly wrong is worse than not showing it at all -- a flow table with silently malformed match fields can misroute production traffic in ways that are hard to debug. Go also genuinely lacks a mature, widely adopted library for the raw OpenFlow wire format, unlike gRPC or MQTT where solid Go clients already exist, so this chapter deliberately builds the surrounding infrastructure instead: the northbound REST API and southbound registration channel a real controller needs.

What is actually the difference between the control plane and the data plane here? The data plane is the part that forwards packets, one hop at a time, as fast as the hardware allows -- in this chapter's example, that role belongs to whatever the switch-side agent runs, whether that is an eBPF program, iptables rules, or a real OpenFlow switch. The control plane is the decision-making layer, which SDN moves out of each individual switch and into the central Controller struct -- it decides what the flow table should contain, and the switches just carry out those decisions.

Why does the southbound listener in the example use port 6653? That is OpenFlow's IANA-assigned port, and the code comment in controller.serveSouthbound(":6653") calls it out for exactly that reason -- using the real assigned port keeps the example's shape consistent with genuine OpenFlow deployments even though the payload traveling over it is simplified JSON rather than OpenFlow's binary framing.

How does this SDN pattern connect to the NFV chapter that comes right after it? The controller/agent split shown here -- centralize decisions, keep the forwarding path dumb and fast, expose a programmable API to the control logic -- is the same architectural idea that shows up in Kubernetes networking plugins, service meshes, and cloud virtual networks. As this chapter's closing section notes, understanding that split pays off directly once NFV asks a related question: what happens when the network functions themselves, not just the forwarding decisions, become software?

Separate the decision from the delivery, and either one can improve without breaking the other.

Key Takeaways

  • SDN separates the control plane (deciding how to forward) from the data plane (forwarding packets), centralizing decisions in a controller talking to dumb, fast switches.
  • OpenFlow is the protocol that popularized this split: match-action flow tables installed and updated by a controller over a defined control channel.
  • Go lacks a mature library for raw OpenFlow framing, but it is a natural fit for the surrounding infrastructure -- a northbound REST API and a southbound agent channel.
  • A JSON-based teaching controller like this chapter's is not a drop-in OpenFlow replacement; real OpenFlow switches need OVSDB or an existing controller like ONOS or OpenDaylight.
  • The same controller/agent split shows up in Kubernetes networking plugins, service meshes, and cloud virtual networks.