CYC-003 · 2026-06 · network / SDN
Segmenting a campus network with SDN
A student in a computer lab shouldn't be able to reach the administration network. On a flat campus network, nothing stops them. I built a three-tier campus in Mininet with VLANs per department, DHCP and DNS on a central router, and a Ryu OpenFlow controller that drops traffic between departments that shouldn't talk. Then I checked it with flow counters and packet captures, and found where it still falls short.
The problem with a flat network
In a flat network every device shares one broadcast domain. A lab PC can ARP for, sniff, and connect to anything on campus, and one misbehaving device floods everyone.
A capture from an earlier version of the topology, before the VLANs were in place, shows it nicely. Pinging from 192.168.10.10 to 192.168.20.10, the router answered every request with an ICMP Redirect:
192.168.10.10 → 192.168.20.10 ICMP Echo (ping) request
192.168.10.1 → 192.168.10.10 ICMP Redirect (Redirect for host)
192.168.20.10 → 192.168.10.10 ICMP Echo (ping) reply
A router sends a redirect when it would forward a packet back out the same interface it arrived on. In other words, it was telling the host "that machine is on your segment, talk to it directly". Two "different" subnets were sharing one wire. That's exactly what segmentation has to fix.
The design
A classic three-tier layout: one core switch, two distribution switches, one access switch per department, and a Linux router r0 doing inter-VLAN routing.
r0 router, one link per VLAN
│
s1 core ← ACLs live here
┌────┴────┐
s2 s3 distribution
┌─┴─┐ ┌─┴─┐
s4 s5 s6 s7 access
│ │ │ │
h1 h2 h3 h4
ECE CS Admin Labs
| VLAN | Department | Subnet | Addressing |
|---|---|---|---|
| 10 | ECE | 192.168.10.0/24 | DHCP .100–.200, also hosts internal DNS |
| 20 | CS | 192.168.20.0/24 | DHCP .100–.200 |
| 30 | Administration | 192.168.30.0/24 | Static: server at .10 |
| 40 | Student labs | 192.168.40.0/24 | DHCP .100–.200 |
The policy: student labs can't reach administration, and ECE can't reach CS. Everything else is allowed.
How it works
1. VLANs on Open vSwitch
Mininet builds the topology, then a script tags ports with ovs-vsctl: host-facing ports are access ports in one VLAN, switch-to-switch links are trunks carrying all four.
ovs-vsctl set port <s7 port to h4> tag=40 # access port: h4 is in VLAN 40
ovs-vsctl set port <s1 port to s3> trunks=10,20,30,40 # trunk to distribution
Ports are looked up by which node they connect to rather than hard-coded, because Mininet's interface numbering depends on link order and silently breaks if you add a link.
The router has four separate links into the core switch, each an access port in one VLAN with that VLAN's gateway address (.1). That's simpler to get working than a single trunk with sub-interfaces ("router on a stick"), at the cost of one port per VLAN.
2. A hybrid controller: let OVS switch, only intervene to block
A pure OpenFlow controller has to install a flow for every forwarding decision. Instead, when each switch connects, the Ryu app installs one lowest-priority rule that hands every packet back to Open vSwitch's built-in switching, which already understands VLAN tags and MAC learning:
match = parser.OFPMatch() # matches everything
actions = [parser.OFPActionOutput(ofproto.OFPP_NORMAL)] # "switch it normally"
self.add_flow(datapath, 0, match, actions) # priority 0 = last resort
Security rules then only have to describe what to drop, at a higher priority. The switches are also set to fail-mode secure: if the controller goes away, they stop forwarding instead of falling back to a plain learning switch with no rules. For a security boundary, failing closed is the right default.
3. The ACLs
match = parser.OFPMatch(
eth_type=0x0800, # IPv4
ipv4_src='192.168.40.0/255.255.255.0', # student labs
ipv4_dst='192.168.30.0/255.255.255.0') # administration
self.add_flow(datapath, 40000, match, []) # no actions = drop
The rules are installed only on the core switch, s1. That's enough because every packet between VLANs has to reach the router, and the router only connects through s1. The drop happens before the packet is ever routed.
4. DHCP and DNS from one router
The router runs one dnsmasq instance per VLAN interface, each bound to its own interface with its own address pool, so a DHCP broadcast in VLAN 40 can only ever be answered from the VLAN 40 pool. The ECE instance also serves an internal zone, cust.local, so hosts can resolve names like admin.cust.local without internet access. The setup script checks every lease it gets: if a host ends up with an address from the wrong VLAN's pool, it reports a VLAN leak instead of carrying on.
5. Routing with FRR
The router runs FRRouting's zebra and ospfd inside its Mininet namespace, with all four subnets in OSPF area 0. There's a limit to what that shows with a single router, covered below.
Verifying it
Every host got an address from the right pool: h1 192.168.10.147, h2 192.168.20.142, h4 192.168.40.121, with the admin server static at 192.168.30.10. The DHCP exchange in the capture takes three milliseconds from Discover to ACK:
0.000000 0.0.0.0 → 255.255.255.255 DHCP Discover
0.000257 192.168.10.1 → 192.168.10.147 DHCP Offer
0.000777 0.0.0.0 → 255.255.255.255 DHCP Request
0.003095 192.168.10.1 → 192.168.10.147 DHCP ACK
Then the actual test: from the lab host, ping the admin server.
mininet> h4 ping -c 3 192.168.30.10
3 packets transmitted, 0 received, 100% packet loss
Packet loss alone doesn't prove the ACL did it; anything broken would look the same. The flow counters do. Before the test, the labs→admin rule had matched nothing:
priority=40000,ip,nw_src=192.168.40.0/24,nw_dst=192.168.30.0/24 n_packets=0 actions=drop
After it, the counter is exactly three, one per ping, while the ECE→CS rule is still at zero:
priority=40000,ip,nw_src=192.168.40.0/24,nw_dst=192.168.30.0/24 n_packets=3 n_bytes=306 actions=drop
priority=40000,ip,nw_src=192.168.10.0/24,nw_dst=192.168.20.0/24 n_packets=0 n_bytes=0 actions=drop
priority=0 n_packets=169 actions=NORMAL
TCP behaves the same way. An HTTP request from the lab to the admin server shows only SYN retransmissions with growing gaps between them and never a reply: the switch drops silently, so the client assumes loss and keeps retrying until it times out. From ECE, which is allowed, the same request completes a normal three-way handshake and gets the page.
Gotchas worth knowing
- Turn off checksum offloading in Mininet. Virtual interfaces leave checksums to "hardware" that doesn't exist, and some daemons, DHCP in particular, drop the resulting packets. The script runs
ethtool --offload <intf> tx off rx offon every interface. - FRR needs its runtime directory. Starting
zebrainside a Mininet namespace crashed on permissions until/var/run/frrwas created first. - Point DHCP clients at a resolver that exists. The first version handed out the admin server as the DNS server, but nothing on it ran DNS, so lookups silently failed. Moving DNS to the router fixed it.
- Give the controller time. With
fail-mode secure, nothing forwards until Ryu has installed its rules, so the script waits for the switches to sync before requesting DHCP leases.
Where it falls short
- The ACL trusts addresses the hosts choose. The rules match on source IP, but a lab machine can set any IP it likes. In principle, a lab host using a spoofed ECE address could get packets through to the admin network one way. I haven't tested this. The fix is to match on something the host doesn't control, such as the VLAN tag or the switch port the packet arrived on, and to turn on strict reverse-path filtering on the router.
- It's stateless, so it blocks both directions. Admin can send to the labs, but the replies come from the lab subnet and hit the drop rule, so admin can't actually use any service there. If one-way access is the goal, it needs connection tracking (Open vSwitch's
ct()action) so replies to allowed connections get through. - IPv4 only. The rules match
eth_type=0x0800. The network doesn't route IPv6, so nothing crosses VLANs over it today, but the policy would have to be extended the moment it did. - OSPF with one router has no one to talk to. It runs and advertises the four subnets, but they're all directly connected, so it isn't learning anything. It becomes meaningful with a second router, for example a redundant core.
- Hard-coded policy. Changing who can talk to whom means editing Python and restarting the controller. A small policy file the controller reads, or Ryu's REST API, would make it something an administrator could actually operate.
code, captures and full report: github.com/Mohsin-Rasul/SDN-Campus-Network