Networking Labs
Five progressive labs covering Linux network namespaces, raw socket programming, eBPF XDP packet processing, TCP performance tuning, and Kubernetes CNI internals. Each lab includes all commands, expected output, and a troubleshooting guide.
Prerequisites:
- Linux system with root access (VM or bare metal)
- Packages: iproute2, tcpdump, iperf3, clang, llvm, bpftool, libelf-dev, libbpf-dev
- Kernel 5.10+ recommended for eBPF XDP support
- For Lab 5: a working Kubernetes cluster (minikube or kind is sufficient)
Lab 1: Network Namespace Experiment
Objective: Create isolated network namespaces, connect them with virtual Ethernet pairs, verify connectivity, and observe how routing tables are namespace-local.
Part A: Create Two Network Namespaces
# Create namespaces
sudo ip netns add ns1
sudo ip netns add ns2
# List them
ip netns list
# Output:
# ns2 (id: 1)
# ns1 (id: 0)
Part B: Create a veth Pair
A veth pair is a virtual Ethernet cable: what goes in one end comes out the other.
# Create a veth pair: veth1 <--> veth2
sudo ip link add veth1 type veth peer name veth2
# Assign each end to a namespace
sudo ip link set veth1 netns ns1
sudo ip link set veth2 netns ns2
# Verify they disappeared from the root namespace
ip link show | grep veth # should show nothing
Part C: Configure Addresses and Bring Up Interfaces
# In ns1: assign 192.168.100.1/24 to veth1
sudo ip netns exec ns1 ip addr add 192.168.100.1/24 dev veth1
sudo ip netns exec ns1 ip link set veth1 up
sudo ip netns exec ns1 ip link set lo up
# In ns2: assign 192.168.100.2/24 to veth2
sudo ip netns exec ns2 ip addr add 192.168.100.2/24 dev veth2
sudo ip netns exec ns2 ip link set veth2 up
sudo ip netns exec ns2 ip link set lo up
# Show addresses
sudo ip netns exec ns1 ip addr show
sudo ip netns exec ns2 ip addr show
Part D: Verify Connectivity and Observe Routing
# Ping from ns1 to ns2
sudo ip netns exec ns1 ping -c 3 192.168.100.2
# Expected output:
# PING 192.168.100.2 (192.168.100.2) 56(84) bytes of data.
# 64 bytes from 192.168.100.2: icmp_seq=1 ttl=64 time=0.083 ms
# 64 bytes from 192.168.100.2: icmp_seq=2 ttl=64 time=0.072 ms
# 64 bytes from 192.168.100.2: icmp_seq=3 ttl=64 time=0.068 ms
# Show routing table in ns1 — namespace-local
sudo ip netns exec ns1 ip route show
# Output: 192.168.100.0/24 dev veth1 proto kernel scope link src 192.168.100.1
# Root namespace has no route to 192.168.100.0/24
ip route show | grep 192.168.100
# (no output — routes are completely isolated)
Part E: Add Internet Access via NAT (Bridge to Default Network)
# Create a bridge in the root namespace
sudo ip link add br0 type bridge
sudo ip addr add 192.168.100.254/24 dev br0
sudo ip link set br0 up
# Create a third veth pair connecting ns1 to the bridge
sudo ip link add veth1-br type veth peer name veth1-ns
sudo ip link set veth1-br master br0
sudo ip link set veth1-br up
sudo ip link set veth1-ns netns ns1
sudo ip netns exec ns1 ip link set veth1-ns up
sudo ip netns exec ns1 ip addr add 192.168.100.10/24 dev veth1-ns
sudo ip netns exec ns1 ip route add default via 192.168.100.254
# Enable IP forwarding and NAT
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -j MASQUERADE
# Test: ns1 should reach the internet
sudo ip netns exec ns1 ping -c 2 8.8.8.8
Cleanup
sudo ip netns del ns1
sudo ip netns del ns2
sudo ip link del br0
Troubleshooting Guide
| Problem | Check | Fix |
|---|---|---|
| ping fails between namespaces | ip netns exec ns1 ip link show — is veth1 UP? |
ip link set veth1 up inside namespace |
ip netns exec fails |
Namespace doesn't exist | ip netns list to verify name |
| No route to host | Check ip route show inside namespace |
Add route: ip route add ... |
| Namespace connectivity works but no internet | IP forwarding disabled | sysctl -w net.ipv4.ip_forward=1 |
Lab 2: Raw Socket Packet Sniffer
Objective: Write a C program that captures all packets on an interface using AF_PACKET, then parse Ethernet, IP, and TCP headers manually. Compare output with tcpdump.
Complete Code
// sniffer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <linux/if_packet.h>
#define SNAP_LEN 65535
void print_mac(unsigned char *mac) {
printf("%02x:%02x:%02x:%02x:%02x:%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
void handle_packet(unsigned char *buf, int len)
{
struct ethhdr *eth = (struct ethhdr *)buf;
struct iphdr *ip;
struct tcphdr *tcp;
struct udphdr *udp;
char src_ip[INET_ADDRSTRLEN], dst_ip[INET_ADDRSTRLEN];
printf("ETH: ");
print_mac(eth->h_source);
printf(" -> ");
print_mac(eth->h_dest);
printf(" proto=0x%04x\n", ntohs(eth->h_proto));
if (ntohs(eth->h_proto) != ETH_P_IP)
return;
ip = (struct iphdr *)(buf + sizeof(struct ethhdr));
inet_ntop(AF_INET, &ip->saddr, src_ip, INET_ADDRSTRLEN);
inet_ntop(AF_INET, &ip->daddr, dst_ip, INET_ADDRSTRLEN);
printf(" IP: %s -> %s proto=%d ttl=%d len=%d\n",
src_ip, dst_ip, ip->protocol, ip->ttl, ntohs(ip->tot_len));
int ip_hdr_len = ip->ihl * 4;
if (ip->protocol == IPPROTO_TCP) {
tcp = (struct tcphdr *)(buf + sizeof(struct ethhdr) + ip_hdr_len);
printf(" TCP: sport=%u dport=%u seq=%u ack=%u flags=%s%s%s%s\n",
ntohs(tcp->source), ntohs(tcp->dest),
ntohl(tcp->seq), ntohl(tcp->ack_seq),
tcp->syn ? "SYN " : "",
tcp->ack ? "ACK " : "",
tcp->fin ? "FIN " : "",
tcp->rst ? "RST " : "");
} else if (ip->protocol == IPPROTO_UDP) {
udp = (struct udphdr *)(buf + sizeof(struct ethhdr) + ip_hdr_len);
printf(" UDP: sport=%u dport=%u len=%u\n",
ntohs(udp->source), ntohs(udp->dest), ntohs(udp->len));
}
}
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "Usage: %s <interface>\n", argv[0]);
return 1;
}
/* AF_PACKET with ETH_P_ALL captures all Ethernet frames */
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket");
return 1;
}
/* Bind to specific interface (optional — without bind, gets all interfaces) */
struct sockaddr_ll addr = {0};
addr.sll_family = AF_PACKET;
addr.sll_protocol = htons(ETH_P_ALL);
addr.sll_ifindex = if_nametoindex(argv[1]);
if (!addr.sll_ifindex) {
fprintf(stderr, "Interface %s not found\n", argv[1]);
close(sock);
return 1;
}
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
close(sock);
return 1;
}
unsigned char buf[SNAP_LEN];
printf("Sniffing on %s (Ctrl+C to stop)...\n\n", argv[1]);
int count = 0;
while (count < 20) { /* capture 20 packets */
int n = recvfrom(sock, buf, SNAP_LEN, 0, NULL, NULL);
if (n < 0) {
perror("recvfrom");
break;
}
printf("--- Packet %d (%d bytes) ---\n", ++count, n);
handle_packet(buf, n);
}
close(sock);
return 0;
}
Build and Test
gcc -O2 -Wall -o sniffer sniffer.c
sudo ./sniffer eth0
# In another terminal, generate traffic
curl -s http://example.com > /dev/null
ping -c 3 8.8.8.8
Expected Output
Sniffing on eth0 (Ctrl+C to stop)...
--- Packet 1 (74 bytes) ---
ETH: 52:54:00:12:34:56 -> ff:ff:ff:ff:ff:ff proto=0x0800
IP: 10.0.2.15 -> 8.8.8.8 proto=1 ttl=64 len=60
--- Packet 2 (60 bytes) ---
ETH: 52:54:00:12:34:56 -> 52:54:00:ab:cd:ef proto=0x0800
IP: 10.0.2.15 -> 93.184.216.34 proto=6 ttl=64 len=40
TCP: sport=54321 dport=80 seq=1234567 ack=0 flags=SYN
Compare with tcpdump
# Run both in parallel — output should describe the same packets
sudo tcpdump -i eth0 -n -c 20 2>/dev/null &
sudo ./sniffer eth0
Troubleshooting Guide
| Problem | Cause | Fix |
|---|---|---|
socket: Operation not permitted |
Not running as root | sudo ./sniffer |
| No packets captured | Wrong interface name | ip link show to find correct name |
| Malformed output | Header parsing offset wrong | Check ihl * 4 for variable IP header length |
| Only seeing ARP, no TCP | IP filtering too aggressive | Check h_proto condition |
Lab 3: eBPF XDP Packet Counter
Objective: Write a minimal BPF program in C that counts packets per protocol, attach it to a network interface using XDP, and read counters from userspace.
Part A: BPF Kernel Program
// xdp_counter.c — loaded into the kernel
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 256); /* indexed by IP protocol number */
} proto_count SEC(".maps");
SEC("xdp")
int xdp_packet_counter(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
__u32 key = ip->protocol;
__u64 *count = bpf_map_lookup_elem(&proto_count, &key);
if (count)
__sync_fetch_and_add(count, 1);
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
Part B: Compile the BPF Program
# Compile to BPF bytecode
clang -O2 -target bpf \
-I /usr/include/x86_64-linux-gnu \
-I /usr/include/bpf \
-c xdp_counter.c \
-o xdp_counter.o
# Inspect the generated bytecode
llvm-objdump -d xdp_counter.o
Part C: Load and Attach with ip
# Attach to interface (XDP_FLAGS_SKB_MODE = software mode, works everywhere)
sudo ip link set dev eth0 xdpgeneric obj xdp_counter.o sec xdp
# Verify attachment
ip link show eth0
# Output includes: prog/xdp id 42 tag abc123def456
# Generate traffic
ping -c 10 8.8.8.8 # ICMP = protocol 1
curl http://example.com # TCP = protocol 6
Part D: Read Per-CPU Counters with bpftool
# List BPF maps
sudo bpftool map list
# Read the proto_count map (assuming map id 5)
sudo bpftool map dump id 5
# Expected output (showing TCP=6, ICMP=1, UDP=17 counts):
# key: 01 00 00 00 value (per-cpu): [10, 0, 0, 0] # 10 ICMP on CPU 0
# key: 06 00 00 00 value (per-cpu): [456, 12, 0, 0] # TCP across CPUs
# key: 11 00 00 00 value (per-cpu): [23, 8, 0, 0] # UDP across CPUs
Part E: Detach the Program
sudo ip link set dev eth0 xdp off
ip link show eth0 # prog/xdp line disappears
Troubleshooting Guide
| Problem | Cause | Fix |
|---|---|---|
clang: error: unknown target |
Wrong clang version | clang --version — need 10+ |
| Verifier rejects program | Out-of-bounds access | Add bounds check before every pointer dereference |
RTNETLINK: Operation not supported |
Old kernel without XDP | Use xdpgeneric (software mode) |
| Map lookup returns wrong counts | PERCPU_ARRAY — need to sum CPUs | Sum values across all CPUs in userspace |
Lab 4: TCP Performance Tuning
Objective: Establish iperf3 baseline, apply kernel TCP tuning parameters, enable BBR, and measure improvement at each step.
Part A: Establish Baseline
# Server (run in background or on a separate host)
iperf3 -s -p 5201 &
# Client: baseline measurement
iperf3 -c localhost -t 30 -P 4 # 4 parallel streams, 30 seconds
# Example baseline output:
# [SUM] 0.00-30.00 sec 8.45 GBytes 2.35 Gbits/sec sender
# [SUM] 0.00-30.00 sec 8.43 GBytes 2.35 Gbits/sec receiver
Record the baseline. Also record:
ss -n | grep ESTAB | wc -l # established connections
cat /proc/net/sockstat # socket memory usage
cat /proc/sys/net/core/rmem_default # default receive buffer (bytes)
Part B: Tune Buffer Sizes
# Check current limits
sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem
# Apply tuning (for 10 Gbps+ networks)
sudo sysctl -w net.core.rmem_max=134217728 # 128 MB
sudo sysctl -w net.core.wmem_max=134217728
sudo sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sudo sysctl -w net.ipv4.tcp_wmem="4096 87380 134217728"
sudo sysctl -w net.core.netdev_max_backlog=5000
sudo sysctl -w net.ipv4.tcp_no_metrics_save=1
# Re-test
iperf3 -c localhost -t 30 -P 4
Typical result: 10–30% throughput improvement for large transfers.
Part C: Enable TCP BBR
BBR (Bottleneck Bandwidth and Round-trip time) is a congestion control algorithm from Google. It significantly outperforms CUBIC on lossy networks and long-haul connections.
# Check current congestion control
sysctl net.ipv4.tcp_congestion_control
# Output: cubic
# Check available algorithms
sysctl net.ipv4.tcp_available_congestion_control
# Enable BBR (requires kernel 4.9+)
sudo modprobe tcp_bbr
echo "tcp_bbr" | sudo tee /proc/sys/net/ipv4/tcp_congestion_control
# Verify
sysctl net.ipv4.tcp_congestion_control
# Output: bbr
# Test again
iperf3 -c localhost -t 30 -P 4
When BBR wins vs. CUBIC:
| Scenario | Winner | Reason |
|---|---|---|
| LAN, zero packet loss | Similar | Both probe bandwidth aggressively |
| WAN, 1% packet loss | BBR | CUBIC halves window on any loss |
| Long RTT (100ms+), good link | BBR | BBR models bandwidth directly |
| High concurrency (1000s of flows) | BBR | Lower buffer occupancy (less bufferbloat) |
Part D: Additional Tuning Knobs
# Enable TCP Fast Open (TFO) — reduces latency for repeat connections
sudo sysctl -w net.ipv4.tcp_fastopen=3
# Increase TCP connection backlog
sudo sysctl -w net.core.somaxconn=65535
# Enable receive packet steering
sudo sysctl -w net.core.rps_sock_flow_entries=32768
# Test final configuration
iperf3 -c localhost -t 60 -P 8
# Compare before/after
echo "Baseline: 2.35 Gbits/sec"
echo "After tuning: (your result)"
Tuning Summary Table
| Parameter | Default | Tuned Value | Effect |
|---|---|---|---|
net.core.rmem_max |
212992 | 134217728 | Maximum socket receive buffer |
net.core.wmem_max |
212992 | 134217728 | Maximum socket send buffer |
net.ipv4.tcp_rmem |
4096 87380 6291456 |
4096 87380 134217728 |
TCP receive buffer range |
net.ipv4.tcp_congestion_control |
cubic | bbr | Congestion algorithm |
net.core.somaxconn |
4096 | 65535 | Listen backlog |
net.ipv4.tcp_tw_reuse |
0 | 1 | Reuse TIME_WAIT sockets |
Troubleshooting Guide
| Problem | Check | Fix |
|---|---|---|
| iperf3 connection refused | Server not running | iperf3 -s -p 5201 in background |
| No throughput improvement after buffer tuning | Application using small buffers | Tune app buffer size too |
| BBR not available | Old kernel or module missing | uname -r check; lsmod | grep bbr |
Lab 5: Kubernetes CNI Walkthrough
Objective: Manually invoke a CNI plugin to understand what happens when a pod is scheduled. Trace the network path from pod to pod using tcpdump.
Part A: Prerequisites
# Install kind (Kubernetes in Docker)
go install sigs.k8s.io/kind@latest
kind create cluster --name cni-lab
# Verify
kubectl get nodes
# NAME STATUS ROLES AGE
# cni-lab-control-plane Ready control-plane 2m
Part B: Observe Existing Pod Network Path
# Create two test pods
kubectl run pod1 --image=busybox --command -- sleep 3600
kubectl run pod2 --image=busybox --command -- sleep 3600
kubectl wait --for=condition=ready pod/pod1 pod/pod2
# Get pod IPs
kubectl get pod -o wide
# NAME READY STATUS IP NODE
# pod1 1/1 Running 10.244.0.5 cni-lab-control-plane
# pod2 1/1 Running 10.244.0.6 cni-lab-control-plane
Part C: Trace Pod Network Path
# Get inside the kind node
docker exec -it cni-lab-control-plane bash
# Inside the node: find veth pairs for pods
ip link show type veth
# Each pod gets one veth end inside its netns and one end in the host
# Find the network namespace for pod1
# CNI stores netns under /var/run/netns/ or linked from /proc/<pid>/ns/net
crictl ps | grep pod1
# Get the container ID
CONTAINER_ID=$(crictl ps | grep pod1 | awk '{print $1}')
crictl inspect $CONTAINER_ID | grep pid
# PID: 12345
# Enter pod1's network namespace
nsenter --net=/proc/12345/ns/net -- ip addr show
# Shows pod1's eth0 with IP 10.244.0.5
# From inside pod1's netns, trace packet route
nsenter --net=/proc/12345/ns/net -- ip route show
# default via 10.244.0.1 dev eth0 ← gateway is the CNI bridge
Part D: Manually Invoke CNI
# CNI plugins live here on kind nodes
ls /opt/cni/bin/
# bridge flannel host-local loopback portmap ...
# CNI config (kind uses kindnet or flannel)
cat /etc/cni/net.d/*.conf
# Manually invoke the bridge CNI plugin
# First, create a network namespace
ip netns add test-ns
# CNI_COMMAND=ADD invokes the plugin
cat << 'EOF' > /tmp/cni-conf.json
{
"cniVersion": "0.4.0",
"name": "mynet",
"type": "bridge",
"bridge": "cni-test0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"subnet": "10.99.0.0/24",
"routes": [{"dst": "0.0.0.0/0"}]
}
}
EOF
CNI_COMMAND=ADD \
CNI_CONTAINERID=test-container-id \
CNI_NETNS=/var/run/netns/test-ns \
CNI_IFNAME=eth0 \
CNI_PATH=/opt/cni/bin \
/opt/cni/bin/bridge < /tmp/cni-conf.json
# Expected JSON output from CNI plugin:
# {
# "cniVersion": "0.4.0",
# "interfaces": [{"name": "cni-test0",...},{"name":"eth0",...}],
# "ips": [{"address": "10.99.0.2/24", "gateway": "10.99.0.1"}]
# }
Part E: Observe veth Pair Creation
# After CNI ADD, observe what was created
ip link show cni-test0 # bridge created
ip link show | grep veth # veth pair in host namespace
ip netns exec test-ns ip addr show eth0 # 10.99.0.2/24 inside netns
# Trace a packet from test-ns to the internet
ip netns exec test-ns ping -c 2 8.8.8.8
# While it runs, capture on the host bridge
tcpdump -i cni-test0 -n icmp
# Output shows ICMP packets crossing the bridge
Part F: Pod-to-Pod tcpdump
# Back to the kubectl environment
# Capture on the node while pods communicate
docker exec -it cni-lab-control-plane tcpdump -i any -n \
src 10.244.0.5 -c 20 2>/dev/null &
kubectl exec pod1 -- ping -c 5 10.244.0.6
# Expected tcpdump output:
# 10:00:01 IP 10.244.0.5 > 10.244.0.6: ICMP echo request
# 10:00:01 IP 10.244.0.6 > 10.244.0.5: ICMP echo reply
# (packets appear twice: on the veth in pod1's netns and on the bridge)
Pod Network Path Summary
pod1 (eth0 10.244.0.5)
└── veth-abc123 (host end)
└── cni0 bridge (10.244.0.1/24)
└── veth-def456 (pod2's host end)
└── pod2 (eth0 10.244.0.6)
All intra-node pod traffic goes: pod1 eth0 → veth → bridge → veth → pod2 eth0. No kernel routing — pure L2 bridging.
Cleanup
# Remove test namespace
ip netns del test-ns
ip link del cni-test0 2>/dev/null
# Delete kind cluster
kind delete cluster --name cni-lab
Troubleshooting Guide
| Problem | Check | Fix |
|---|---|---|
| Pod stuck in ContainerCreating | kubectl describe pod → CNI error |
Check CNI plugin logs: journalctl -u kubelet |
| No connectivity between pods | Route missing on node | ip route show on node; check CNI config |
| CNI ADD returns error | Plugin config invalid | Check JSON syntax; verify ipam block |
| tcpdump shows packets on bridge but pod can't ping | iptables DROP rule | iptables -L -n -v | grep DROP |
Lab Appendix: Key Commands Reference
| Goal | Command |
|---|---|
| List all network namespaces | ip netns list |
| Execute in a namespace | ip netns exec <ns> <cmd> |
| Show veth pairs | ip link show type veth |
| Show bridge members | bridge link show |
| Show all routes | ip route show table all |
| Show socket statistics | ss -ntp |
| Capture packets | tcpdump -i eth0 -n -w cap.pcap |
| Read capture | tcpdump -r cap.pcap -n |
| Show BPF programs | bpftool prog list |
| Show BPF maps | bpftool map list |
| XDP attach | ip link set dev eth0 xdpgeneric obj prog.o sec xdp |
| XDP detach | ip link set dev eth0 xdp off |