I virtualized network functions, a router, a firewall, and a load balancer, as isolated Linux network namespaces connected by veth pairs, then chained them into a single service path. Running VNFs in software on commodity hardware replaced dedicated appliances for the lab fabric.

Objective & Context

NFV decouples network functions from proprietary hardware, running them as software instances that can be chained into service function paths. This lab uses namespaces as lightweight VNFs and HAProxy as a virtual load balancer, the same primitives behind container networking.

Learning Objectives

  • Create isolated network namespaces and link them with veth pairs.
  • Run a firewall VNF inside a namespace using nftables.
  • Run HAProxy as a load-balancer VNF in a second namespace.
  • Chain the VNFs into a working service function path.
  • Explain how NFV replaces fixed appliances with software instances.

Environment & Prerequisites

Prerequisites

  • Linux host with iproute2, nftables, and HAProxy.
  • Privileges to create namespaces and veth pairs.
  • Two backend services to balance.
  • Linux host with iproute2, nftables, and HAProxy (sudo apt install -y haproxy).
  • Root or sudo access to create namespaces and veth pairs.
  • Two backend services to balance (simple Python HTTP servers are enough).
  • Understanding of IP addressing and basic firewalling concepts.

Step-by-Step Execution

1. Create VNF namespaces and link them [PRIVILEGED]

ip netns add fw && ip netns add lb ip link add veth-host type veth peer name veth-fw2 ip link add veth-fw type veth peer name veth-lb ip link set veth-fw2 netns fw && ip link set veth-fw netns fw && ip link set veth-lb netns lb ip addr add 192.168.100.1/30 dev veth-host && ip link set veth-host up ip netns exec fw ip addr add 192.168.100.2/30 dev veth-fw2 ip netns exec fw ip addr add 10.0.0.1/30 dev veth-fw ip netns exec lb ip addr add 10.0.0.2/30 dev veth-lb ip netns exec fw ip link set lo up \; ip netns exec fw ip link set veth-fw2 up \; ip netns exec fw ip link set veth-fw up ip netns exec lb ip link set lo up \; ip netns exec lb ip link set veth-lb up ip route add 10.0.0.0/30 via 192.168.100.2 ip netns exec lb ip route add default via 10.0.0.1

Expected output:

(no output on success: every ip command here is silent)
ip netns exec fw ip -br addr show

Expected output:

lo               UNKNOWN        127.0.0.1/8
veth-fw2         UP             192.168.100.2/30
veth-fw          UP             10.0.0.1/30

What this teaches: namespaces plus veth pairs are the lightweight primitives behind containers, VNFs, and virtual routers alike.

Creating a veth pair leaves both ends in the current namespace: each end must be moved with ip link set ... netns, addressed, and brought up, or the link stays DOWN and nothing routes. The host (client side) reaches the service network through 192.168.100.2, and the lb namespace returns traffic through 10.0.0.1.

2. Run a firewall VNF in its namespace

ip netns exec fw nft add table inet filter ip netns exec fw nft 'add chain inet filter forward { type filter hook forward priority 0; policy drop; }' ip netns exec fw nft add rule inet filter forward ct state established,related accept ip netns exec fw nft add rule inet filter forward tcp dport 80 accept ip netns exec fw sysctl -w net.ipv4.ip_forward=1

Expected output:

net.ipv4.ip_forward = 1
ip netns exec fw nft list ruleset

Expected output:

table inet filter {
	chain forward {
		type filter hook forward priority 0; policy drop;
		ct state established,related accept
		tcp dport 80 accept
	}
}

What this teaches: a firewall VNF is just packet-filter rules scoped to one namespace's interfaces.

The add rule command fails on a fresh system unless the table and chain already exist: create them first, exactly as above. The chain uses policy drop with an established/related rule so return traffic passes; only new connections to TCP 80 are permitted. ip_forward is a per-namespace setting, so it must be enabled inside the fw namespace. Confirm forwarding between the links with: ip netns exec fw ping -c 2 10.0.0.2.

3. Run HAProxy as the load-balancer VNF

Config content (save as /etc/haproxy/lb.cfg; install first with sudo apt install -y haproxy):

global
    daemon
defaults
    mode http
    timeout connect 5s
    timeout client 30s
    timeout server 30s
frontend web
    bind 10.0.0.2:80
    default_backend web
backend web
    balance roundrobin
    server b1 127.0.0.11:8001 check
    server b2 127.0.0.12:8002 check
haproxy -c -f /etc/haproxy/lb.cfg

Expected output:

Configuration file is valid
ip netns exec lb ip addr add 127.0.0.11/8 dev lo ip netns exec lb ip addr add 127.0.0.12/8 dev lo ip netns exec lb python3 -m http.server 8001 --bind 127.0.0.11 >/tmp/b1.log 2>&1 & ip netns exec lb python3 -m http.server 8002 --bind 127.0.0.12 >/tmp/b2.log 2>&1 & ip netns exec lb haproxy -f /etc/haproxy/lb.cfg ip netns exec lb ss -tlnp | grep ':80'

Expected output:

(haproxy prints nothing: the daemon directive backgrounds it)
LISTEN 0      128    10.0.0.2:80    0.0.0.0:*

What this teaches: chaining VNFs turns independent functions into an ordered service path, the core NFV idea.

Always validate the config with haproxy -c before starting: a typo otherwise kills the process silently. The backends are two Python web servers on loopback aliases inside the lb namespace (any 127.x address is valid on lo). HAProxy health checks (check) mark a backend down if its server stops responding.

4. Test the full service chain [PRIVILEGED]

curl -s http://10.0.0.2/ | head -4 curl -s http://10.0.0.2:22 --max-time 3 || echo "port 22 blocked by the firewall VNF"

Expected output:

<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
port 22 blocked by the firewall VNF

What this teaches: the chain is only real when traffic provably traverses it: the request crossed the firewall VNF, hit the HAProxy frontend, and was served by a backend.

HTTP through the chain returns a directory listing (proving the firewall passed TCP 80 and the LB distributed to a backend); SSH to port 22 times out (proving the drop policy works). Repeat the curl a few times and watch the backend logs alternate between b1 and b2 (roundrobin). Cleanup when done: sudo pkill -f 'http.server 800' \; ip netns exec lb pkill haproxy \; sudo ip netns del fw \; sudo ip netns del lb.

How It Works

Network Function Virtualization decouples network functions from proprietary hardware. Instead of buying a firewall appliance and a load-balancer appliance, you run firewall software and load-balancer software as virtualized functions on commodity servers, chaining them into service function paths that traffic traverses in order.

Linux network namespaces provide the isolation primitive: each namespace has its own interfaces, addresses, routes, and firewall rules, invisible to other namespaces. A veth pair connects two namespaces like a virtual patch cable. This is exactly how container networking works under the hood, so the lab doubles as a container-networking primer.

Service chaining is about ordering. The client hits the firewall namespace first, which filters; surviving packets move to the load-balancer namespace, which distributes them across backends. Each VNF is independently replaceable and scalable, which is the operational payoff of NFV over fixed appliances.

Validation & Testing

Send requests through the chain and confirm they traverse firewall then load balancer and distribute across backends. Pass criteria: only permitted traffic passes the firewall VNF and load is shared across both backends.

Troubleshooting

SymptomLikely causeFix
Namespaces cannot ping each otherveth ends not assigned to the right namespaces or missing addressesVerify with ip netns exec <ns> ip addr and confirm each veth end has an address in the same subnet
nft rule has no effectRule added to the wrong table, chain, or namespaceList the ruleset inside the namespace with ip netns exec fw nft list ruleset
HAProxy fails to bindAnother process holds the port, or the config has an errorRun haproxy -c -f /etc/haproxy/lb.cfg to check the config and ss -tlnp to find the port holder
Traffic bypasses the firewall VNFRouting sends packets directly to the LB namespaceCheck routes in each namespace; the firewall namespace must be on the path
Everything breaks after rebootNamespaces and veth pairs are ephemeral by designScript the setup so the whole chain rebuilds with one command
Advanced: Troubleshooting
  • No traffic between namespaces: assign IPs to both veth ends and bring them up.
  • HAProxy bind fails: confirm it binds inside the namespace, not the host.
  • Forwarding blocked: enable forwarding within the namespace.

Key Results

  • Chained 3 virtual network functions into a single service path.
  • Replaced dedicated appliances with software VNFs on commodity hardware.
  • Load-balanced traffic across 2 backends through the HAProxy VNF.
  • Isolated each function in its own namespace for clean teardown.

Key Takeaways

  • Namespaces and veth pairs are the universal building blocks of virtual networking.
  • Each VNF should do one job well so the chain stays debuggable.
  • Verify each hop independently before testing the full chain.
  • Script ephemeral network state; never rely on hand-built namespaces surviving a reboot.

Practice Challenges

Challenge 1: Add a NAT VNF

Goal: Insert a third namespace that performs NAT between the firewall VNF and the backends.

Hint: Use nft masquerade in the NAT namespace and point the firewall's route at it.

Challenge 2: Make HAProxy health-aware

Goal: Configure HAProxy health checks so a dead backend is removed from rotation automatically.

Hint: Add option httpchk to the backend servers and watch the stats socket during a backend outage.