Back
Loading views...Network Virtualization
Network Virtualization
PART 1: THE THREE FUNDAMENTAL PACKET TECHNIQUES
Every packet processing system uses one or more of these three core techniques:
- Packet Processing — Examining and modifying packet headers/payloads
- Packet Bypass — Avoiding normal kernel networking stack for performance
- Packet Filtering — Allowing or denying packets based on rules
Let's explore each technique across all execution modes and machine types.
TECHNIQUE 1: PACKET PROCESSING
Definition
Packet processing means reading packet headers, making decisions, and potentially modifying the packet (rewriting headers, encapsulating, decapsulating, fragmenting, etc.).
1.1 Packet Processing in KERNEL MODE
What is Kernel Mode?
- Code runs at CPU privilege level 0 (Ring 0)
- Direct hardware access
- No memory protection (crash = system crash)
- Context: interrupt handlers, softirq, kernel threads
How Packet Processing Works in Kernel
Packet arrives at NIC → DMA to memory → Interrupt → Kernel takes over
Step-by-step in kernel:
1. HARDWARE INTERRUPT (hardirq context):
- NIC raises interrupt line
- CPU jumps to interrupt handler (NIC driver)
- Driver acknowledges interrupt
- Driver schedules NET_RX_SOFTIRQ
- Returns from interrupt (~1-5µs)
2. SOFTIRQ CONTEXT (ksoftirqd or in-interrupt softirq):
- net_rx_action() called
- Driver's poll() function called (NAPI)
- For each packet:
a. Allocate sk_buff structure (kernel packet descriptor)
b. DMA packet data into sk_buff buffer
c. Set sk_buff metadata:
- skb->dev = receiving interface
- skb->protocol = ethertype (0x0800 for IPv4)
- skb->mac_header, network_header, transport_header offsets
d. Call netif_receive_skb(skb)
3. PACKET RECEIVE PROCESSING (netif_receive_skb):
- __netif_receive_skb_core()
- Check for rx_handler (OVS, bridge, bonding register handlers here)
- Deliver to protocol handler based on skb->protocol:
- ETH_P_IP (0x0800) → ip_rcv()
- ETH_P_ARP (0x0806) → arp_rcv()
- ETH_P_8021Q (0x8100) → vlan_handler
4. IP LAYER PROCESSING (ip_rcv):
- Validate IP header checksum
- Netfilter PREROUTING hook
iptables rules in PREROUTING chain executed here
- Routing decision: ip_route_input()
a. Lookup destination IP in FIB (Forwarding Information Base)
Routing table stored as LC-trie (level-compressed trie)
Longest prefix match: O(log n) where n = number of routes
b. Result: LOCAL (for this host) or FORWARD (route to another interface)
5a. IF LOCAL (packet destined for this host):
- Netfilter LOCAL_IN hook
- Deliver to L4 protocol:
- ip->protocol == 6 (TCP) → tcp_v4_rcv()
- ip->protocol == 17 (UDP) → udp_rcv()
- L4 processing finds socket (4-tuple lookup: src_ip, dst_ip, src_port, dst_port)
- Copy data to socket receive buffer
- Wake up application waiting on recv() syscall
5b. IF FORWARD (packet being routed):
- Netfilter FORWARD hook
- Decrement TTL, recompute IP checksum
- ARP resolution for next hop (if not in cache)
- Netfilter POSTROUTING hook
- Transmit: ip_finish_output() → qdisc (traffic control) → NIC driver
6. NETFILTER HOOKS (iptables/nftables rules):
- Each hook point (PREROUTING, INPUT, FORWARD, POSTROUTING, OUTPUT)
- For each rule in chain:
- Match packet against rule criteria (source IP, dest port, etc.)
- If match: execute action (ACCEPT, DROP, DNAT, SNAT, LOG, etc.)
- If no match: continue to next rule
- Connection tracking (conntrack):
- Hash table lookup on 5-tuple
- State: NEW, ESTABLISHED, RELATED, INVALID
- For SNAT/DNAT: store translation in conntrack table
Data structures:
struct sk_buff {
struct sk_buff *next, *prev; // Linked list
struct net_device *dev; // Interface
unsigned char *head, *data, *tail, *end; // Buffer pointers
__u16 transport_header, network_header, mac_header; // Offsets
__be16 protocol; // Ethertype
__u32 mark; // Netfilter mark
struct dst_entry *dst; // Routing decision
};
Kernel Mode Packet Processing on Different Machines
PHYSICAL MACHINE (Kernel Mode)
Flow: NIC → DMA → Kernel → Application
Hardware: Real NIC (Intel, Mellanox, Broadcom)
Driver: igb, ixgbe, mlx5_core (kernel modules)
Process:
1. NIC DMA to RAM (physical address)
2. Interrupt to CPU
3. Kernel allocates sk_buff from slab allocator
4. Full kernel network stack (as described above)
5. Deliver to application socket buffer
Performance: ~10-50µs latency, 5-10 Gbps per core
VIRTUAL MACHINE (Kernel Mode)
Flow: Physical NIC → Hypervisor Kernel → vSwitch → VM Kernel → Application
TWO KERNELS INVOLVED:
- Hypervisor kernel (host kernel, KVM/ESXi/Hyper-V)
- Guest kernel (inside VM)
Example: KVM with virtio-net
HOST SIDE (Hypervisor Kernel):
1. Physical NIC → DMA → host sk_buff
2. Host kernel processes (as above)
3. Packet reaches OVS or Linux bridge (running in host kernel)
4. Bridge/OVS forwards to tap device or vhost-net
5. vhost-net kernel module:
- Writes packet to virtqueue (shared memory ring buffer)
- Signals VM via eventfd or MSI-X interrupt injection
GUEST SIDE (VM Kernel):
6. VM receives interrupt from hypervisor
7. virtio-net driver in guest kernel reads virtqueue
8. Allocates guest sk_buff
9. Full guest kernel network stack (ip_rcv, netfilter in guest, etc.)
10. Delivers to application in VM
Performance: ~50-200µs latency (includes VM exit/entry), 2-5 Gbps
Overhead: Double kernel processing (host + guest)
CONTAINER (Kernel Mode)
Flow: NIC → Kernel → veth → Container netns
SINGLE KERNEL, MULTIPLE NETWORK NAMESPACES:
Process:
1. Physical NIC → DMA → host kernel sk_buff
2. Host kernel processes (ip_rcv, routing)
3. Routing decision: "send to 172.17.0.2"
Route lookup: 172.17.0.2/32 dev veth-abc (learned from container)
4. Packet sent to veth pair:
- veth is a virtual ethernet pair: veth-host <-> veth-container
- Packet written to veth-host → appears on veth-container
5. veth-container exists in container's network namespace
- Container netns has separate:
- Routing table
- iptables rules
- Interface list
- Socket table
6. Kernel processes packet IN THE CONTAINER NETNS:
- ip_rcv() again (but using container's routing table)
- iptables rules in container netns
- Deliver to socket in container's socket table
7. Application in container receives via normal recv() syscall
Performance: ~10-30µs latency (veth overhead ~5µs), 8-15 Gbps
Key insight: SAME KERNEL, just different namespace contexts
KUBERNETES POD (Kernel Mode)
Flow: NIC → Kernel → CNI plugin logic → veth → Pod netns
K8s pod = container(s) sharing one network namespace
Example: Calico CNI in BGP mode
Process:
1. Physical NIC → host kernel
2. Routing lookup: "destination is pod IP 192.168.1.5"
Route installed by Calico: 192.168.1.5/32 dev cali1234567
3. cali1234567 is veth pair: cali1234567 (host) <-> eth0 (in pod netns)
4. Before packet goes to veth, iptables processing:
- Felix (Calico agent) has installed iptables chains:
iptables -A cali-tw-cali1234567 -m set --match-set cali-allowed-sources src -j ACCEPT
iptables -A cali-tw-cali1234567 -j DROP
- "cali-tw" = "to workload" (ingress to pod)
- Policy enforcement happens HERE in kernel iptables
5. If ACCEPT: packet sent to veth pair → enters pod netns
6. In pod netns:
- Interface eth0 (the veth end in pod)
- IP 192.168.1.5/32 assigned to eth0
- Default route: 169.254.1.1 dev eth0 (magic proxy ARP route)
- Kernel delivers to application socket
Performance: ~20-100µs (iptables overhead + veth), 5-15 Gbps
Overhead: NetworkPolicy = iptables rule evaluation
1.2 Packet Processing in USER MODE
What is User Mode?
- Code runs at CPU privilege level 3 (Ring 3)
- Memory protection (segfault instead of kernel panic)
- Cannot directly access hardware
- Must use syscalls to interact with kernel
How Packet Processing Works in User Mode
Application needs to send/receive packets → syscall → kernel
RECEIVING (Application perspective):
1. Application calls recv() or read():
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, ...);
listen(sockfd, ...);
int conn = accept(sockfd, ...);
recv(conn, buffer, size, 0); ← SYSCALL
2. SYSCALL MECHANISM:
- CPU executes syscall instruction
- Switch from Ring 3 (user) to Ring 0 (kernel)
- Save user context (registers, stack pointer)
- Jump to kernel syscall handler
- Syscall overhead: ~100-200ns
3. IN KERNEL (during syscall):
- Look up socket by file descriptor
- Check socket receive buffer
- If data available:
- Copy from kernel socket buffer to user buffer
- Copy overhead: ~1-3µs for small packets
- Return to userspace with byte count
- If no data:
- Put process to sleep (TASK_INTERRUPTIBLE)
- Scheduler switches to another process
- When packet arrives: kernel wakes process
- Return to userspace
4. Return to user mode:
- Restore user context
- Application continues with data in buffer
SENDING (Application perspective):
1. Application calls send() or write():
send(sockfd, buffer, size, 0); ← SYSCALL
2. IN KERNEL (during syscall):
- Allocate sk_buff
- Copy data from user buffer to sk_buff
- Build TCP/IP headers
- Pass through network stack (routing, netfilter, qdisc)
- Hand to NIC driver
- Return to userspace (may return before actual transmission)
ZERO-COPY OPTIMIZATIONS (reduce user-kernel copying):
sendfile():
sendfile(socket_fd, file_fd, offset, count);
- Kernel reads file directly to socket buffer
- No copy to userspace
- Used by nginx, Apache for static files
splice():
- Move data between file descriptors via kernel pipe
- No userspace involvement
io_uring (modern):
- Shared ring buffers between kernel and userspace
- Application writes I/O requests to submission queue
- Kernel processes, writes results to completion queue
- Batches syscalls: one syscall for many operations
- Latency: ~1-2µs vs ~5-10µs for traditional syscalls
User Mode Packet Processing on Different Machines
PHYSICAL MACHINE (User Mode)
Application runs directly on physical OS
Example: nginx web server
Process:
1. nginx listens on port 80:
socket(), bind(), listen(), accept() → kernel creates socket
2. Client connects → kernel TCP handshake (SYN, SYN-ACK, ACK)
All in kernel, no nginx involvement yet
3. Kernel accepts connection → returns new socket to nginx
4. nginx calls epoll_wait() → blocks waiting for data
5. Packet arrives → kernel processes (NIC → kernel stack → socket buffer)
6. Kernel wakes nginx (epoll event)
7. nginx calls recv() → syscall → kernel copies data to nginx buffer
8. nginx processes HTTP request (all in userspace)
9. nginx calls sendfile(socket, file_fd) → kernel sends file
10. Kernel transmits response → NIC → wire
Performance: ~50-100µs application latency
Benefit: No virtualization overhead
Drawback: Application can crash entire system if it has kernel bug
VIRTUAL MACHINE (User Mode)
Application runs in userspace INSIDE the VM
Example: Application in Ubuntu VM on KVM hypervisor
Process:
1. Application in VM does socket(), bind(), listen()
→ Syscall to GUEST KERNEL
2. Guest kernel creates socket (in guest kernel space)
3. Packet arrives:
Physical NIC → Host kernel → OVS → vhost-net → virtqueue
4. Guest kernel interrupt → virtio-net driver → guest sk_buff
5. Guest kernel processes → delivers to socket buffer (in guest kernel)
6. Application calls recv() → syscall to guest kernel
Guest kernel copies from guest socket buffer to application buffer
7. Application processes in VM userspace
Performance: ~100-300µs application latency
Overhead:
- Guest userspace → guest kernel (syscall ~100ns)
- Guest kernel → host kernel (VM exit ~1-5µs)
- Host kernel → guest kernel (VM entry ~1-5µs)
Key: Application doesn't know it's in a VM!
CONTAINER (User Mode)
Application runs in userspace in container's namespace
Example: nginx in Docker container
Process:
1. Container starts:
docker run -p 8080:80 nginx
Docker creates network namespace for container
2. nginx in container does socket(), bind(80), listen()
→ Syscall to HOST KERNEL (same kernel as host!)
But syscall executes in CONTAINER'S NETWORK NAMESPACE context
3. Socket created in container's namespace
Port 80 binding is isolated to container namespace
(Host can also have something on port 80 - different namespace)
4. Packet arrives:
NIC → host kernel → docker0 bridge → veth pair → container netns
5. Host kernel delivers to socket in container namespace
6. nginx calls recv() → syscall (to same host kernel)
Kernel copies to nginx buffer
7. nginx processes in container userspace (same as any process)
Performance: ~50-150µs application latency
Key difference from physical:
- Same kernel
- Just different namespace context
- Minimal overhead (~5-20µs for veth crossing)
Docker port mapping (8080:80):
- iptables DNAT rule on host:
-A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
- External client → host:8080 → DNAT → 172.17.0.2:80 (container)
KUBERNETES POD (User Mode)
Application runs in container(s) in pod
Example: Python Flask app in K8s pod
Process:
1. Pod starts: kubectl run flask --image=flask-app
Kubelet calls CNI plugin (e.g., Calico)
CNI creates veth pair, assigns IP (192.168.1.5)
2. Application in pod:
app.run(host='0.0.0.0', port=5000)
→ Syscall to host kernel (in pod's netns context)
3. Socket bound to 192.168.1.5:5000 (pod IP)
4. Service created:
apiVersion: v1
kind: Service
spec:
selector: {app: flask}
ports: [{port: 80, targetPort: 5000}]
clusterIP: 10.96.0.100
5. kube-proxy installs iptables rules:
-A KUBE-SERVICES -d 10.96.0.100 -p tcp --dport 80 -j KUBE-SVC-XXX
-A KUBE-SVC-XXX -j DNAT --to-destination 192.168.1.5:5000
6. Client pod sends to 10.96.0.100:80
→ Host kernel iptables DNAT → 192.168.1.5:5000
→ Routing → cali1234567 veth
→ Delivers to socket in flask pod's netns
7. Flask app recv() → syscall → data copy
8. Flask processes request in userspace
Performance: ~100-200µs (iptables + veth + policy)
Components:
- kube-proxy (userspace daemon managing iptables)
- CNI plugin (userspace daemon managing routes/policy)
- Application (userspace in container)
All userspace components talking to kernel via syscalls
1.3 Packet Processing in BYPASS MODE
What is Bypass Mode?
Techniques that avoid the standard kernel network stack for performance:
- DPDK (userspace poll-mode drivers)
- AF_XDP (XDP sockets)
- Hardware offload (NIC does processing)
DPDK (Data Plane Development Kit)
KEY CONCEPT: Application directly accesses NIC, kernel is OUT of data path
Architecture:
Application → DPDK libraries → PMD (Poll Mode Driver) → NIC registers → NIC
Setup:
1. Reserve hugepages (2MB or 1GB pages):
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
Why: Reduces TLB misses, DMA works better with large contiguous memory
2. Unbind NIC from kernel driver:
echo 0000:01:00.0 > /sys/bus/pci/drivers/ixgbe/unbind
Why: Kernel can't interfere with DPDK control of NIC
3. Bind NIC to VFIO (Virtual Function I/O) or UIO (Userspace I/O):
echo vfio-pci > /sys/bus/pci/devices/0000:01:00.0/driver_override
echo 0000:01:00.0 > /sys/bus/pci/drivers/vfio-pci/bind
Why: Allows userspace to access PCI device safely
4. Application initializes DPDK EAL (Environment Abstraction Layer):
rte_eal_init(argc, argv);
Discovers NICs, maps NIC registers to userspace virtual memory
Packet Processing Flow:
1. NIC receives packet → DMA to hugepage memory
2. NIC updates RX descriptor ring (in hugepage memory)
3. DPDK application POLLS RX ring (while(1) loop, CPU 100% busy):
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, mbufs, BURST_SIZE);
- No interrupt
- No kernel
- No syscall
- Just reads memory that NIC updated
4. Application gets rte_mbuf (DPDK packet descriptor):
struct rte_mbuf {
void *buf_addr; // Virtual address of packet data
rte_iova_t buf_iova; // Physical/IOVA address (for DMA)
uint16_t data_off; // Offset to packet start
uint16_t pkt_len; // Packet length
uint64_t ol_flags; // Offload flags
...
};
5. Application processes packet (parse headers, classify, modify):
struct ether_hdr *eth = rte_pktmbuf_mtod(mbuf, struct ether_hdr *);
struct ipv4_hdr *ip = (struct ipv4_hdr *)(eth + 1);
// Modify IP destination
ip->dst_addr = new_dst;
// Recompute checksum (or use NIC offload)
6. Application transmits:
rte_eth_tx_burst(port_id, queue_id, &mbuf, 1);
- Writes to TX descriptor ring
- NIC DMAs from hugepage
- Transmits
7. No return to kernel at any point!
Performance:
- Latency: 1-5µs (application to application)
- Throughput: 10-24 Mpps per core (64-byte packets)
- CPU: Core is 100% busy polling (acceptable for dedicated appliances)
Data structures:
- rte_mbuf: Pre-allocated from mempool (no malloc during processing)
- rte_mempool: Memory pool of rte_mbufs in hugepage memory
- Descriptor rings: Circular buffers shared between app and NIC
DPDK on Different Machines
PHYSICAL MACHINE (DPDK)
Dedicated physical server running DPDK application
Example: VNF router using DPDK
Setup:
- Physical server with 2x 25G NICs
- Ubuntu with DPDK packages
- CPU cores isolated: isolcpus=2-9 (for DPDK)
Flow:
1. Boot: kernel loads on core 0,1
2. DPDK app starts on cores 2-9:
./router -l 2-9 -n 4 -- -p 0x3
(-l: logical cores, -n: memory channels, -p: port mask)
3. DPDK binds NIC0 (port 0) and NIC1 (port 1) to vfio-pci
4. Kernel can no longer see these NICs
5. Packet arrives at NIC0 → DMA to hugepage → DPDK app polls
6. DPDK app routes: lookup in hash table (rte_hash)
struct ipv4_hdr *ip = ...;
uint32_t next_hop = rte_hash_lookup(routing_table, &ip->dst_addr);
7. Transmit out NIC1: rte_eth_tx_burst(1, 0, &mbuf, 1);
Performance: 40+ Mpps forwarding, <3µs latency
Use case: NFV routers, firewalls, load balancers (no general-purpose OS needed)
VIRTUAL MACHINE (DPDK in VM)
VM running DPDK application, hypervisor uses vhost-user
Example: vRouter VNF in OpenStack
Setup:
- Host runs OVS-DPDK (host DPDK process)
- VM configured with vhost-user interface (not virtio-net):
<interface type='vhostuser'>
<source type='unix' path='/var/run/openvswitch/vhost-user-1' mode='client'/>
</interface>
- Hugepages allocated on host and guest
Flow:
1. Packet arrives at physical NIC
2. Host OVS-DPDK processes (PMD thread polling physical NIC)
3. OVS flow lookup → forward to VM
4. OVS writes to vhost-user ring buffer (shared hugepage memory):
- vhost-user = userspace (not kernel vhost-net)
- Shared memory between OVS-DPDK process and QEMU
- QEMU maps guest memory
5. VM's DPDK application polls vhost-user ring (via virtio-pmd in DPDK):
rte_eth_rx_burst(virtio_port_id, 0, mbufs, 32);
- No VM exit!
- Polling shared memory
6. VM DPDK processes packet
7. VM transmits: rte_eth_tx_burst() → writes to vhost-user TX ring
8. OVS-DPDK PMD polls vhost-user TX ring → transmits out physical NIC
Performance: 8-15 Mpps per VM, ~10-30µs latency
Benefit: Much faster than virtio-net (no kernel, no VM exits for data)
Limitation: VM must run DPDK (not standard kernel networking)
CONTAINER (DPDK in Container)
DPDK running inside container
Example: Containerized VNF (CNF - Cloud-Native Network Function)
Setup:
- Container needs:
- Access to hugepages: -v /dev/hugepages:/dev/hugepages
- Privileged mode OR specific capabilities: --cap-add=IPC_LOCK,SYS_ADMIN
- CPU pinning: --cpuset-cpus=2-5
- NIC bound to vfio-pci on host (or SR-IOV VF passed to container)
Flow:
1. Container starts with DPDK app
2. DPDK app initializes EAL inside container
3. App mmaps hugepages (shared with host via mount)
4. App accesses NIC via VFIO:
Option A: Entire physical NIC (exclusive, not shareable)
Option B: SR-IOV VF assigned to container namespace
5. Polling loop runs (CPU 100% on cores 2-5)
6. Process same as physical DPDK
Performance: Same as physical (~40 Mpps)
Container overhead: Nearly zero (it's just a namespace, same kernel)
Challenge: Resource isolation (DPDK is greedy with CPU and memory)
KUBERNETES (DPDK in Pod)
DPDK VNF as a Kubernetes pod (SR-IOV device plugin)
Example: DPDK-based vRouter in K8s
Setup:
- Node has SR-IOV NIC
- SR-IOV device plugin running (DaemonSet):
Discovers VFs, advertises to kubelet as resources
- Pod spec requests SR-IOV VF:
resources:
requests:
intel.com/sriov_netdevice: '1'
limits:
intel.com/sriov_netdevice: '1'
hugepages-1Gi: 2Gi
Flow:
1. Kubelet sees pod needs SR-IOV VF
2. SR-IOV device plugin:
- Allocates VF from pool
- Moves VF into pod's network namespace:
ip link set <vf> netns <pod-netns>
3. Pod's DPDK app starts:
- Binds VF to vfio-pci (inside pod)
- DPDK EAL discovers VF
4. DPDK app polls VF directly (bypass kernel entirely)
5. Process same as physical
Performance: Same as physical (VF has dedicated hardware queue)
Benefit: K8s orchestration + DPDK performance
Drawback: Complex (needs SR-IOV, device plugin, hugepages, CPU manager)
TECHNIQUE 2: PACKET BYPASS
Definition
Bypass means avoiding the kernel network stack to achieve higher performance by:
- Userspace I/O (DPDK, covered above)
- Kernel bypass at driver level (XDP)
- Hardware offload (NIC does the work)
2.1 XDP (eXpress Data Path) — Kernel Bypass at Driver Level
What is XDP?
- eBPF programs attached at NIC driver receive path
- Runs BEFORE sk_buff allocation
- Can drop, redirect, or pass packets with minimal overhead
- Kernel code, but executes at the earliest possible point
Normal kernel: NIC → DMA → sk_buff alloc → netif_receive_skb → ip_rcv → ...
↑
~100ns+ just for sk_buff allocation
XDP path: NIC → DMA → XDP program → decision (100-500ns total)
↑
XDP runs here, before ANY kernel overhead
XDP Actions
XDP_DROP: Drop packet immediately (DDoS mitigation)
XDP_PASS: Pass to kernel (normal path)
XDP_TX: Reflect packet back out same interface (simple switch)
XDP_REDIRECT: Send to different interface or AF_XDP socket
XDP_ABORTED: Error condition (malformed packet)
XDP Program Example (DDoS Protection)
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
// BPF map: blacklist of source IPs
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1000000);
__type(key, __u32); // Source IP
__type(value, __u8); // Dummy value
} blacklist SEC(".maps");
SEC("xdp")
int xdp_drop_blacklist(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
// Parse Ethernet header
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) // Bounds check (verifier requirement)
return XDP_ABORTED;
// Check if IP packet
if (eth->h_proto != htons(ETH_P_IP))
return XDP_PASS;
// Parse IP header
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_ABORTED;
// Check blacklist
__u32 src_ip = ip->saddr;
if (bpf_map_lookup_elem(&blacklist, &src_ip)) {
return XDP_DROP; // Drop at driver level, no sk_buff allocated
}
return XDP_PASS; // Not blacklisted, continue to kernel
}
char _license[] SEC("license") = "GPL";
# Compile XDP program
clang -O2 -target bpf -c xdp_drop.c -o xdp_drop.o
# Load and attach to interface
ip link set dev eth0 xdp obj xdp_drop.o sec xdp
# Add IP to blacklist (from userspace)
bpftool map update name blacklist key 0x01020304 value 0x00
# This blocks 4.3.2.1 (hex is little-endian)
# Packet from 4.3.2.1 arrives:
# NIC → DMA → XDP program sees src_ip=0x01020304
# Map lookup finds it → return XDP_DROP
# Packet dropped at driver, ~50ns processing, 0 kernel overhead
XDP on Different Machines
PHYSICAL MACHINE (XDP)
XDP program attached to physical NIC
Example: DDoS mitigation at edge router
Setup:
- Physical server with XDP-capable NIC (most modern NICs)
- Load XDP program: ip link set eth0 xdp obj program.o
Flow:
1. Packet arrives at NIC
2. NIC DMAs to memory (RX ring buffer)
3. Driver calls XDP hook BEFORE netif_receive_skb():
act = bpf_prog_run_xdp(xdp_prog, xdp_md);
4. XDP program executes (eBPF JIT-compiled to native code):
- Has access to xdp_md->data (packet bytes)
- Can read/modify packet
- Makes decision (DROP, PASS, TX, REDIRECT)
5. Based on action:
- XDP_DROP: Driver frees buffer immediately, no kernel processing
- XDP_PASS: Driver continues normal path (allocate sk_buff, etc.)
- XDP_TX: Driver queues packet for TX on same interface
- XDP_REDIRECT: Packet goes to specified target
Performance:
- DROP: ~50-100ns per packet, 20-24 Mpps per core
- PASS: Same as normal kernel
- TX: ~100-200ns, 10-20 Mpps per core
Use case: DDoS protection, fast packet filtering, load balancing (Facebook's Katran)
VIRTUAL MACHINE (XDP in guest or host)
XDP can run in TWO places in VM scenario:
Option A: XDP in GUEST (on virtio-net interface inside VM)
- VM has virtio-net interface (eth0)
- Attach XDP to eth0 inside VM:
(inside VM) ip link set eth0 xdp obj program.o
- Packets arriving at VM's virtio-net trigger XDP
- Performance: Moderate (still has vhost overhead before XDP runs)
- Use: Guest wants to filter its own traffic
Option B: XDP on HOST (on tap/vhost interface)
- Attach XDP to tap device or physical NIC on host
- Filters before packet even reaches VM
- Performance: Best for dropping unwanted VM traffic
- Use: Hypervisor-level filtering
Example (XDP on host tap):
VM's tap device: tap0
(on host) ip link set tap0 xdp obj filter.o
Packet to VM → host NIC → OVS → tap0 → XDP (DROP or PASS) → vhost → VM
If XDP drops: VM never sees packet, zero VM overhead
CONTAINER (XDP on veth)
XDP can attach to veth interfaces
Example: Per-container XDP firewall
Setup:
- Container has veth pair: veth-abc (host) <-> eth0 (container)
- Attach XDP to host-side veth:
ip link set veth-abc xdp obj filter.o
Flow:
1. Packet to container → routed to veth-abc
2. Before veth transmits to container, XDP runs:
XDP program checks packet, DROP or PASS
3. If PASS: veth transmits to container
Performance: ~200-500ns overhead (veth + XDP)
Benefit: Per-container filtering without iptables overhead
Limitation: XDP on veth is "generic XDP" (software, not driver-level)
- Generic XDP runs after sk_buff allocation
- Slower than native XDP (but faster than iptables)
KUBERNETES (XDP with Cilium)
Cilium uses XDP for high-performance filtering
Setup:
- Cilium agent loads XDP programs on:
- Physical host interface (for node-level filtering)
- veth pairs (for pod-level filtering)
Flow (NodePort service with XDP acceleration):
1. External packet arrives at node IP:port
2. XDP program on physical NIC sees packet
3. XDP checks: Is this a NodePort service?
- Lookup in BPF map: (dst_ip, dst_port) → Service
4. If yes: XDP REDIRECTS to backend pod veth directly
- No iptables
- No kube-proxy
- Packet goes: NIC → XDP → redirect → veth → pod
5. If no: XDP_PASS to kernel
Performance: ~40-60 Gbps service load balancing (vs 10-20 Gbps with iptables)
Benefit: Bypasses entire host network stack for forwarded traffic
2.2 Hardware Offload — Ultimate Bypass
SmartNIC / Hardware OVS
Packet processing done entirely in NIC hardware (ASIC or FPGA)
Example: Mellanox ConnectX-5/6 with OVS offload
Capability:
- NIC has embedded switch ASIC (eSwitch)
- Supports TC (Traffic Control) flower offload
- OVS rules installed in NIC hardware
Setup:
ethtool -K eth0 hw-tc-offload on
ovs-vsctl set Open_vSwitch . other_config:hw-offload=true
Flow:
1. OVS adds flow:
ovs-ofctl add-flow br0 "ip,nw_src=10.0.0.1,nw_dst=10.0.0.2,actions=output:2"
2. OVS kernel module tries to offload to NIC:
- Translates OpenFlow to TC flower
- Sends to NIC driver via tc filter API
3. NIC driver programs hardware:
- Match: src_ip=10.0.0.1, dst_ip=10.0.0.2
- Action: forward to port 2 (VF or physical port)
4. Packet arrives at NIC:
- NIC ASIC matches in hardware TCAM (ns latency)
- NIC forwards directly (CPU sees nothing!)
5. Host CPU load: ~0% for forwarded traffic
Performance: Line rate (100 Gbps), <1µs latency
CPU: Freed up for application work
Limitation:
- TCAM size limited (4K-32K flows)
- Not all OVS actions supported (vendor-specific)
- Complex flows fall back to software
Hardware OVS on Different Machines
PHYSICAL MACHINE (SmartNIC)
Server with Mellanox ConnectX-6 (100G SmartNIC)
Setup:
- Physical ports: 2x 100G QSFP
- SR-IOV enabled: 128 VFs
- OVS-kernel with hw-offload
Use case: NFV infrastructure node
Flow (VM1 to VM2 on same host):
1. Both VMs have SR-IOV VFs
2. OVS rule: VF1 → VF2 (installed in NIC hardware)
3. VM1 sends packet → VF1 → NIC eSwitch (hardware)
4. NIC eSwitch forwards to VF2 → VM2
5. Host CPU: Not involved at all (0% load)
VM-to-external:
1. OVS rule: VF1 → VXLAN encap → physical port
2. NIC hardware does VXLAN encapsulation
3. Packet sent out 100G port
4. CPU: Still not involved
Performance: 100 Gbps VM-to-VM, <500ns latency
VIRTUAL MACHINE (VM on SmartNIC host)
VM benefits from host's SmartNIC offload
Scenario: OpenStack compute node with SmartNIC
Setup:
- Host: Ubuntu with OVS, SmartNIC
- VM: Ubuntu with virtio-net
- Neutron creates OVS flows for VM
Flow:
1. VM1 sends to VM2 (different compute node)
2. VM1 virtio-net → vhost-user → OVS (host)
3. OVS flow: encap VXLAN, send to remote host
4. This flow is OFFLOADED to SmartNIC
5. vhost-user hands packet to NIC → NIC encaps → wire
6. Host CPU: Only vhost-user overhead (~10% CPU)
vs. 60-80% CPU without offload
Benefit: VMs get line-rate networking without host CPU cost
Drawback: VM doesn't control offload (host does)
TECHNIQUE 3: PACKET FILTERING
Definition
Filtering is deciding whether to allow or deny packets based on rules.
3.1 Packet Filtering in KERNEL MODE
iptables / Netfilter
Netfilter is the kernel framework, iptables is the userspace tool
Architecture:
- 5 hook points in IP stack: PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING
- Tables: filter (default), nat, mangle, raw, security
- Chains: built-in (INPUT, FORWARD, OUTPUT) or custom
- Rules: match criteria + action (ACCEPT, DROP, REJECT, LOG, etc.)
Rule evaluation:
- Linear chain walk: O(n) where n = number of rules
- For each packet, each rule checked sequentially
- First matching rule wins (unless rule is non-terminating like LOG)
Example firewall:
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # Allow SSH
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # Allow HTTP
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -j DROP # Default deny
Packet processing:
1. Packet arrives → Netfilter PREROUTING hook
2. For each rule in PREROUTING chain:
- Match protocol, port, state, etc.
- If match: execute action (jump to chain, ACCEPT, DROP, etc.)
- If no match: continue to next rule
3. Routing decision
4. Netfilter INPUT hook (if local) or FORWARD (if routing)
5. Same rule walk for INPUT/FORWARD
Performance:
- 10 rules: ~1µs overhead
- 100 rules: ~5-10µs overhead
- 1000 rules: ~50-100µs overhead (degraded badly)
- 10,000 rules: Minutes to load, seconds per packet (unusable)
Connection Tracking (conntrack)
Stateful firewall magic: remember connection state
How it works:
1. First packet of connection (SYN):
- Netfilter creates conntrack entry:
(src_ip, src_port, dst_ip, dst_port, proto) → state=NEW
- Hash table: 5-tuple hashed, entry stored
2. Reply packet (SYN-ACK):
- Conntrack lookup by 5-tuple (reversed)
- State updated: NEW → ESTABLISHED
3. Subsequent packets:
- Conntrack lookup: ESTABLISHED
- iptables rule: -m conntrack --ctstate ESTABLISHED -j ACCEPT
- Single rule accepts all ESTABLISHED traffic!
4. FIN or timeout:
- Connection closed or idle timeout
- Conntrack entry removed
Performance cost:
- Hash table lookup: ~500ns
- State update: ~200ns
- Total: ~1-2µs per packet
Table exhaustion:
- Default max: 65,536 entries (tunable to 1M+)
- High-churn workloads (microservices): millions of connections
- Symptom: "nf_conntrack: table full, dropping packet"
- Solution: Increase nf_conntrack_max, reduce timeouts, or bypass with eBPF
iptables Filtering on Different Machines
PHYSICAL MACHINE (iptables)
Firewall on physical server
Example: Web server with iptables firewall
Rules:
iptables -P INPUT DROP # Default deny
iptables -A INPUT -i lo -j ACCEPT # Loopback
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT # SSH from LAN
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # HTTP
iptables -A INPUT -p tcp --dport 443 -j ACCEPT # HTTPS
iptables -A INPUT -j LOG --log-prefix "DROP: " # Log drops
iptables -A INPUT -j DROP
Flow:
1. External client → server:443
2. Packet arrives → NIC → sk_buff → ip_rcv → Netfilter INPUT
3. Rule walk:
- Rule 1 (lo): No match (not loopback)
- Rule 2 (ESTABLISHED): No match (NEW connection)
- Rule 3 (SSH): No match (dest port 443, not 22)
- Rule 4 (HTTP): No match (dest port 443, not 80)
- Rule 5 (HTTPS): MATCH! Action: ACCEPT
4. Packet continues to nginx application
Performance: ~2-5µs filtering overhead (5 rules checked)
VIRTUAL MACHINE (iptables in guest)
VM runs its own iptables
Scenario: Ubuntu VM on KVM host
Flow:
1. Packet arrives: Physical NIC → host OVS → vhost-net → VM
2. Inside VM guest kernel:
- virtio-net driver receives → guest sk_buff
- Guest ip_rcv → Guest Netfilter INPUT hook
- Guest iptables rules evaluated (VM's own firewall)
3. If guest iptables ACCEPTS: deliver to application in VM
Layering:
- Host can ALSO have iptables rules on tap device
- Packet filtered TWICE: host iptables, then guest iptables
Example (two-layer filtering):
Host: iptables -A FORWARD -o tap0 -p tcp --dport 3389 -j DROP # Block RDP to VM
Guest: iptables -A INPUT -p tcp --dport 22 -j ACCEPT # Allow SSH in VM
Result: RDP blocked at host (never reaches VM), SSH allowed if reaches VM
CONTAINER (host iptables)
Container traffic filtered by HOST iptables
Docker creates iptables rules automatically:
When you run: docker run -p 8080:80 nginx
Docker creates:
iptables -t nat -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
iptables -A DOCKER -d 172.17.0.2 -p tcp --dport 80 -j ACCEPT
Per-container chains:
iptables -N DOCKER-USER # For user-added rules
iptables -N DOCKER # For Docker-managed rules
Flow:
1. External → host:8080
2. PREROUTING: DNAT to 172.17.0.2:80
3. FORWARD chain: Check DOCKER chain
4. DOCKER chain: Allow to 172.17.0.2:80
5. Packet sent to docker0 bridge → veth pair → container
User firewall:
iptables -I DOCKER-USER -s 192.168.1.0/24 -j DROP # Block subnet
Performance degradation with many containers:
- 10 containers: ~10µs
- 100 containers: ~50-100µs (100 chains to check)
- Solution: Use nftables or Cilium (eBPF)
KUBERNETES (iptables NetworkPolicy via Calico)
NetworkPolicy implemented via iptables
Example NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-allow-http
spec:
podSelector: {app: web}
policyTypes: [Ingress]
ingress:
- from:
- podSelector: {app: frontend}
ports:
- protocol: TCP
port: 80
Calico Felix translates to iptables:
# Create IPSet for frontend pods
ipset create cali-frontend-pods hash:ip
ipset add cali-frontend-pods 192.168.1.5 # frontend pod IPs
ipset add cali-frontend-pods 192.168.1.6
# Create iptables rules
iptables -N cali-tw-web-pod # "to-workload" chain
iptables -A cali-tw-web-pod -p tcp --dport 80 -m set --match-set cali-frontend-pods src -j ACCEPT
iptables -A cali-tw-web-pod -j DROP
# Jump to per-pod chain
iptables -A FORWARD -o cali-web-pod -j cali-tw-web-pod
Flow (frontend pod → web pod):
1. Frontend pod sends to web pod IP
2. Host kernel routes to web pod's veth
3. Netfilter FORWARD hook
4. Jump to cali-tw-web-pod chain
5. Check: src IP in cali-frontend-pods IPSet? Yes → ACCEPT
6. Packet sent to web pod
Flow (other pod → web pod):
1. Other pod sends to web pod
2-4. Same
5. Check: src IP in IPSet? No → DROP
6. Packet dropped
Performance with 1000 pods:
- IPSet lookup: ~O(1) (hash table)
- Total overhead: ~5-10µs per packet
- Much better than 1000 separate IP rules (which would be ~500µs)
3.2 Packet Filtering in USER MODE
User-mode Firewalls (Application-level)
Filtering done by userspace application, not kernel
Examples:
- Application-level: nginx with geo-blocking, rate limiting
- Proxy-based: Squid, HAProxy with ACLs
- WAF (Web Application Firewall): ModSecurity, AWS WAF
nginx geo-blocking (User Mode)
nginx.conf:
geo $block_country {
default 0;
91.0.0.0/8 1; # Block range (example: Russia)
112.0.0.0/8 1; # Block range
}
server {
listen 80;
if ($block_country) {
return 403;
}
...
}
Flow:
1. TCP connection established (kernel accepts, no filtering yet)
2. nginx accepts connection (accept() syscall)
3. nginx receives HTTP request (recv() syscall)
4. nginx reads client IP from socket: getpeername()
5. nginx checks geo database: Is IP in blocked range?
6. If yes: nginx sends "HTTP/1.1 403 Forbidden", closes socket
7. If no: nginx processes request normally
Key: Filtering happens AFTER TCP connection established
CPU/memory used for every connection (even blocked ones)
Performance: ~100µs per request overhead (geo lookup)
vs. kernel iptables: ~2µs
Why use it: Layer 7 awareness (HTTP headers, URL, etc.)
User-mode Filtering on Different Machines
PHYSICAL MACHINE (User-mode Firewall)
Example: Squid proxy with ACLs
squid.conf:
acl internal_network src 192.168.0.0/16
acl blocked_sites dstdomain .facebook.com .twitter.com
http_access deny blocked_sites
http_access allow internal_network
http_access deny all
Flow:
1. Client connects to Squid (TCP)
2. Squid accepts (kernel allows, no filtering)
3. Client sends HTTP CONNECT or GET
4. Squid parses HTTP request (userspace)
5. Squid checks ACLs:
- Is destination facebook.com? Yes → deny (send 403)
6. Squid closes client connection
All in userspace, full protocol parsing
VIRTUAL MACHINE (User-mode in VM)
Example: Application in VM with built-in rate limiting
Scenario: API server in VM with request rate limiting
Code (Python Flask):
from flask import Flask
from flask_limiter import Limiter
app = Flask(__name__)
limiter = Limiter(app, key_func=lambda: request.remote_addr)
@app.route('/api')
@limiter.limit("10 per minute")
def api():
return "OK"
Flow:
1. Client → VM → Flask receives request
2. Flask limiter checks Redis/memory:
- How many requests from this IP in last 60s?
- If >= 10: return HTTP 429 (Too Many Requests)
- If < 10: Process request, increment counter
All in application userspace (inside VM)
Performance: ~500µs overhead (Redis lookup)
CONTAINER (User-mode Filter in Container)
Example: Envoy proxy sidecar in pod
Envoy config:
rate_limits:
- actions:
- request_headers:
header_name: "x-user-id"
descriptor_key: "user_id"
limits:
- per_header_limit: 100
time_unit: MINUTE
Flow:
1. Request → pod → Envoy sidecar (localhost:15001)
2. Envoy receives HTTP request
3. Envoy checks rate limit:
- Extracts x-user-id header
- Checks local cache: Has user_id made 100 requests in last minute?
- If yes: Return 429
- If no: Proxy to application container
All in Envoy userspace (inside pod's container)
3.3 Packet Filtering in BYPASS MODE
eBPF/XDP Filtering
Filtering at driver level or TC hook using eBPF
Advantages over iptables:
- O(1) map lookups vs O(n) rule chains
- Runs at driver (XDP) or TC hook
- Programmable: can implement custom logic
Example: XDP blacklist (shown earlier)
- Hash map of blocked IPs
- XDP program: lookup in map, drop if found
- ~50ns per packet vs ~5µs for iptables
PHYSICAL MACHINE (XDP Filtering)
Example: DDoS protection
XDP program attached to physical NIC
- Blocks 1M malicious IPs
- Hash map: 1M entries
- Performance: 20 Mpps drop rate (vs 2 Mpps with iptables)
KUBERNETES (Cilium eBPF NetworkPolicy)
Cilium replaces iptables with eBPF
NetworkPolicy → Cilium translates to eBPF program
BPF program on veth:
SEC("tc/ingress")
int policy_check(struct __sk_buff *skb) {
__u32 src_identity = skb->cb[CB_SRC_ID]; # Set by Cilium agent
__u32 dst_identity = skb->cb[CB_DST_ID];
struct policy_key key = {src_identity, dst_identity, dst_port, proto};
struct policy_val *val = bpf_map_lookup_elem(&policy_map, &key);
if (!val || val->action == DROP)
return TC_ACT_SHOT;
return TC_ACT_OK;
}
Performance:
- Map lookup: O(1), ~100ns
- vs iptables: O(n), ~5µs for 100 rules
- 50x faster
PART 2: ANOMALY DEEP-DIVES
Now we solve the two complex packet journey scenarios you requested.
ANOMALY 1: Windows Host → VirtualBox → Ubuntu VM → K8s Pod
SCENARIO:
Physical machine: Windows (kernel0, user0)
└─ VirtualBox hypervisor
└─ Ubuntu VM (kernel1, user1)
└─ Kubernetes (running in VM)
└─ Pod (container)
└─ Application
QUESTION: Packet arrives at physical NIC → how does it reach the application?
Complete Packet Journey with Techniques & Modes
Stage 1: Physical NIC → Windows Kernel (KERNEL0 MODE)
1. PACKET ARRIVES AT PHYSICAL NIC:
Hardware: Intel Ethernet NIC (e.g., e1000, I219)
2. NIC DMA:
- NIC reads packet from wire
- NIC DMAs packet data to Windows memory (physical address)
- NIC raises interrupt (MSI-X)
3. WINDOWS KERNEL INTERRUPT HANDLER (KERNEL0, Ring 0):
- CPU jumps to NDIS (Network Driver Interface Specification) driver
- Driver acknowledges interrupt
- Driver schedules DPC (Deferred Procedure Call) - Windows equivalent of softirq
Technique: PACKET PROCESSING (Windows TCP/IP stack)
Mode: KERNEL0 (Windows kernel)
4. WINDOWS DPC (still KERNEL0):
- NDIS miniport driver processes packet
- Creates NET_BUFFER structure (Windows equivalent of sk_buff)
- Passes to Windows TCP/IP stack (tcpip.sys)
5. WINDOWS TCP/IP STACK:
- Checks if packet is for local host or for routing
- Destination: VirtualBox VM's IP (assigned to VirtualBox NAT/bridged interface)
- Routing decision: Forward to VirtualBox virtual switch
6. WINDOWS FIREWALL (KERNEL0):
- Windows Filtering Platform (WFP) checks rules
- If allowed: Continue
Technique: PACKET FILTERING (WFP in kernel)
Mode: KERNEL0
Stage 2: Windows Kernel → VirtualBox Hypervisor (USER0 MODE)
7. VIRTUALBOX VIRTUAL SWITCH:
- VirtualBox runs as userspace process on Windows (VirtualBox.exe)
- BUT: Packet forwarding happens via kernel driver (VBoxDrv.sys)
VBoxDrv.sys is a Windows kernel driver that:
- Creates virtual network adapters
- Bridges traffic between Windows host network and VM
8. PACKET FORWARDED TO VBOXDRV.SYS (KERNEL0):
- Windows routing sends packet to VirtualBox virtual adapter
- VBoxDrv.sys receives packet (kernel driver)
Technique: PACKET PROCESSING (forwarding)
Mode: KERNEL0 (Windows kernel driver)
9. VBOXDRV → VIRTUALBOX.EXE NOTIFICATION:
- VBoxDrv.sys signals VirtualBox.exe (userspace) that packet arrived
- VirtualBox.exe reads packet from shared memory
Technique: USER0 MODE receives notification
Mode: USER0 (VirtualBox hypervisor process)
Stage 3: VirtualBox Hypervisor → Ubuntu VM (KERNEL1 MODE)
10. VIRTUALBOX VIRTUAL NIC EMULATION (USER0):
- VirtualBox.exe emulates network card for VM (e.g., Intel PRO/1000 MT Desktop)
- Packet placed in emulated NIC's receive buffer
- VirtualBox injects virtual interrupt into Ubuntu VM
Technique: PACKET PROCESSING (NIC emulation)
Mode: USER0 (VirtualBox process)
11. UBUNTU VM RECEIVES INTERRUPT (KERNEL1 MODE):
- VM's kernel (kernel1) receives virtual interrupt
- VM's NIC driver (e.g., e1000) interrupt handler runs
- Driver reads from virtual NIC's register (actually VirtualBox memory)
Mode: KERNEL1 (Ubuntu VM kernel, Ring 0 IN THE VM)
12. UBUNTU VM NETWORK STACK (KERNEL1):
- VM kernel allocates sk_buff
- Copies packet data from VirtualBox shared memory
- Processes through Linux network stack:
- netif_receive_skb()
- ip_rcv()
- Netfilter PREROUTING
- Routing decision
Technique: PACKET PROCESSING (Linux kernel network stack)
Mode: KERNEL1
13. ROUTING IN UBUNTU VM:
- Destination IP: Pod IP (e.g., 192.168.1.5)
- Routing table lookup: Installed by Kubernetes CNI
- Route: 192.168.1.5/32 dev cali1234567 (Calico veth interface)
Stage 4: Ubuntu VM Kernel → Kubernetes Pod (KERNEL1 + USER1)
14. CNI VETH INTERFACE (KERNEL1):
- Packet routed to cali1234567 (host-side veth)
- This is still in KERNEL1 (Ubuntu VM's kernel)
Technique: PACKET PROCESSING (veth forwarding)
Mode: KERNEL1
15. IPTABLES NETWORKPOLICY CHECK (KERNEL1):
- Before packet goes to veth, Netfilter FORWARD hook
- Calico iptables rules:
iptables -A cali-tw-cali1234567 -m set --match-set cali-allowed src -j ACCEPT
iptables -A cali-tw-cali1234567 -j DROP
- Checks source IP against IPSet
- If ACCEPT: Continue
Technique: PACKET FILTERING (iptables)
Mode: KERNEL1
16. VETH PAIR CROSSING:
- Packet written to cali1234567 (host side of veth)
- Appears on eth0 (pod side of veth)
- eth0 exists in pod's network namespace (different routing table, different socket table)
- Still same KERNEL1, just different namespace context
Technique: PACKET PROCESSING (namespace traversal)
Mode: KERNEL1 (in pod netns context)
17. POD NETWORK NAMESPACE (KERNEL1):
- Packet arrives on eth0 in pod
- Pod's routing table used:
ip route in pod: default via 169.254.1.1 dev eth0
- Destination IP matches pod IP (192.168.1.5) assigned to eth0
- Kernel delivers to socket
18. APPLICATION RECEIVES (USER1):
- Application in pod called recv() earlier (blocked waiting)
- Kernel wakes application
- Kernel copies data from socket buffer to application buffer (user1 memory)
- recv() returns to application
Technique: PACKET PROCESSING (final delivery)
Mode: USER1 (application in container)
Summary of Anomaly 1 Journey
┌──────────────────────────────────────────────────────────────────────┐
│ PACKET JOURNEY: Physical NIC → Windows → VirtualBox → Ubuntu → K8s │
└──────────────────────────────────────────────────────────────────────┘
NIC → [KERNEL0: Windows TCP/IP + WFP filtering]
→ [KERNEL0: VBoxDrv.sys forwarding]
→ [USER0: VirtualBox.exe NIC emulation]
→ [KERNEL1: Ubuntu VM network stack + iptables filtering]
→ [KERNEL1: CNI veth + NetworkPolicy iptables]
→ [KERNEL1: Pod netns delivery]
→ [USER1: Application recv()]
Techniques used:
- PACKET PROCESSING: At every kernel layer (Windows, VBox, Ubuntu, CNI)
- PACKET FILTERING: Windows WFP (kernel0), Ubuntu iptables (kernel1), NetworkPolicy (kernel1)
- NO BYPASS: All standard kernel paths
Modes:
- KERNEL0: Windows kernel (TCP/IP, WFP, VBoxDrv.sys)
- USER0: VirtualBox hypervisor (NIC emulation, VM management)
- KERNEL1: Ubuntu VM kernel (full Linux network stack, iptables, CNI)
- USER1: Application in Kubernetes pod
Why no bypass:
- Windows/VirtualBox don't support DPDK
- Nested virtualization (VM in VM via Windows) adds overhead
- Standard kernel path is only option
Performance:
- Latency: ~500µs - 2ms (very high due to nested virtualization)
- Throughput: ~1-2 Gbps (Windows → VBox → VM overhead)
- Bottleneck: VirtualBox NIC emulation (USER0) and double kernel (kernel0 + kernel1)
ANOMALY 2: Ubuntu Physical → K8s Pod → VM in Pod → Application
SCENARIO:
Physical machine: Ubuntu (kernel0, user0)
└─ Kubernetes (running directly on Ubuntu)
└─ Pod (container)
└─ KubeVirt VM (virtualized inside pod)
└─ Application
QUESTION: Packet arrives at physical NIC → how does it reach application in VM-in-pod?
This is a VM running INSIDE a container — kubevirt/kata containers architecture.
Complete Packet Journey
Stage 1: Physical NIC → Ubuntu Kernel (KERNEL0 MODE)
1. PACKET ARRIVES AT PHYSICAL NIC:
Hardware: Physical Ethernet NIC (e.g., Intel X710, Mellanox ConnectX)
2. NIC DMA + INTERRUPT (KERNEL0):
- NIC DMAs packet to memory
- Raises interrupt
- Driver interrupt handler (ixgbe, mlx5_core)
- Schedules NET_RX_SOFTIRQ
Technique: PACKET PROCESSING (standard kernel RX)
Mode: KERNEL0 (Ubuntu kernel)
3. SOFTIRQ PROCESSING (KERNEL0):
- net_rx_action() → NAPI poll
- Driver allocates sk_buff
- netif_receive_skb()
- Passes to IP layer
4. UBUNTU KERNEL IP PROCESSING (KERNEL0):
- ip_rcv()
- Netfilter PREROUTING
- Routing decision
5. ROUTING LOOKUP (KERNEL0):
- Destination IP: Pod IP (e.g., 10.244.1.5)
- CNI has installed route:
10.244.1.5/32 dev veth-abc
- Decision: Forward to veth-abc
Technique: PACKET PROCESSING (routing)
Mode: KERNEL0
Stage 2: Kernel → Pod Network Namespace (KERNEL0, different netns)
6. NETFILTER FORWARD + CNI POLICY (KERNEL0):
- Packet going to pod → Netfilter FORWARD hook
- CNI iptables rules (e.g., Calico):
iptables -A cali-fw-veth-abc -m set --match-set allowed-sources src -j ACCEPT
- NetworkPolicy enforcement happens here
Technique: PACKET FILTERING (iptables NetworkPolicy)
Mode: KERNEL0
7. VETH PAIR TRANSMISSION (KERNEL0):
- Packet sent to veth-abc (host side)
- Crosses veth boundary
- Appears on eth0 (pod side, in pod's netns)
Technique: PACKET PROCESSING (veth crossing)
Mode: KERNEL0 (now in pod netns context)
8. POD NETWORK NAMESPACE (KERNEL0):
- Interface: eth0 in pod netns
- IP: 10.244.1.5 assigned to eth0
- Routing in pod: default via 169.254.1.1 dev eth0
- Packet destination matches eth0 IP
- But... there's no application directly listening here!
- Instead: kubevirt VM is running
Stage 3: Pod → KubeVirt VM (Complex!)
9. KUBEVIRT ARCHITECTURE:
Inside the pod, kubevirt runs:
- virt-launcher process (USER0): QEMU/KVM wrapper
- QEMU process (USER0): VM hypervisor
- Linux KVM kernel module (KERNEL0): Hardware virtualization
10. PACKET INTERCEPTION IN POD (USER0):
- virt-launcher has created TAP device in pod's netns:
ip tuntap add tap0 mode tap
- TAP device connected to VM
- Packet routing in pod netns:
Destination 10.244.1.5 → actually TAP device subnet
OR: iptables DNAT to TAP device IP
Example pod routing:
10.244.1.100/32 dev tap0 (VM's IP inside pod)
iptables -t nat -A PREROUTING -d 10.244.1.5 -j DNAT --to 10.244.1.100
Technique: PACKET PROCESSING (DNAT + routing)
Mode: KERNEL0 (in pod netns)
11. TAP DEVICE WRITE (KERNEL0 → USER0):
- Packet routed to tap0
- TAP device is character device (/dev/net/tun)
- QEMU process has open file descriptor on tap0
- Kernel writes packet to TAP device
- QEMU reads from tap0 (read() syscall returns with packet)
Technique: PACKET PROCESSING (kernel → user via TAP)
Mode: KERNEL0 writes, USER0 reads
12. QEMU RECEIVES PACKET (USER0):
- QEMU process running in pod
- QEMU read packet from tap0
- QEMU has emulated NIC for VM (virtio-net)
- QEMU writes packet to virtqueue (shared memory with VM)
- QEMU injects interrupt into VM (via KVM ioctl)
Technique: PACKET PROCESSING (virtio-net emulation)
Mode: USER0 (QEMU process in pod)
Stage 4: KubeVirt VM Kernel → Application (KERNEL1 + USER1)
13. VM RECEIVES INTERRUPT (KERNEL1):
- VM's kernel (kernel1) receives virtio interrupt
- virtio-net driver processes
- Reads packet from virtqueue
- Allocates sk_buff in VM
Technique: PACKET PROCESSING (virtio RX path)
Mode: KERNEL1 (VM's kernel)
14. VM KERNEL NETWORK STACK (KERNEL1):
- ip_rcv() in VM kernel
- Netfilter in VM (if VM has firewall)
- Routing in VM
- Destination: Application IP/port
Technique: PACKET PROCESSING (full network stack)
Technique: PACKET FILTERING (if VM has iptables)
Mode: KERNEL1
15. APPLICATION IN VM RECEIVES (USER1):
- Application called recv() (blocked)
- VM kernel wakes application
- Copies from socket buffer to application buffer
- recv() returns
Mode: USER1 (application in VM)
Summary of Anomaly 2 Journey
┌───────────────────────────────────────────────────────────────────────┐
│ PACKET JOURNEY: Physical NIC → Ubuntu → K8s Pod → VM → Application │
└───────────────────────────────────────────────────────────────────────┘
NIC → [KERNEL0: Ubuntu network stack + Netfilter]
→ [KERNEL0: CNI veth + iptables NetworkPolicy]
→ [KERNEL0: Pod netns → TAP device]
→ [USER0: QEMU reads TAP, virtio-net emulation]
→ [KERNEL1: VM kernel network stack]
→ [USER1: Application in VM]
Techniques:
- PACKET PROCESSING: Every layer (host kernel, veth, TAP, QEMU, VM kernel)
- PACKET FILTERING: NetworkPolicy (kernel0 iptables), VM firewall (kernel1 iptables)
- NO BYPASS: Standard kernel paths (though kubevirt could use vhost-user for performance)
Modes:
- KERNEL0: Ubuntu host kernel (network stack, CNI, pod netns, TAP device)
- USER0: QEMU process in pod (TAP reader, virtio-net emulation)
- KERNEL1: VM's kernel (full Linux network stack)
- USER1: Application inside VM
Special notes:
- TAP device is the bridge: KERNEL0 writes, USER0 (QEMU) reads
- QEMU runs in USER0 (inside pod's namespace, but still userspace)
- VM sees QEMU's virtio-net as real NIC (doesn't know it's nested)
Performance:
- Latency: ~200-500µs (veth ~10µs + TAP ~50µs + virtio ~100µs + VM stack ~50µs)
- Throughput: ~5-10 Gbps (limited by virtio and double network stack)
- Bottleneck: TAP device read/write (crosses kernel-user boundary)
Possible optimization (BYPASS mode):
- Use vhost-user instead of TAP:
- QEMU and OVS share memory (hugepages)
- Eliminates TAP device overhead
- Performance: ~15-20 Gbps
- Technique: BYPASS (vhost-user shared memory, no kernel)
Visual Comparison of Both Anomalies
ANOMALY 1 (Windows → VirtualBox → Ubuntu VM → K8s Pod):
NIC → [Win Kernel0] → [VBox User0] → [Ubuntu Kernel1] → [Pod User1]
↑ ↑ ↑ ↑
Standard NIC Full Linux Application
Windows emulation network stack in container
TCP/IP + CNI/iptables
ANOMALY 2 (Ubuntu → K8s Pod → KubeVirt VM → App):
NIC → [Ubuntu Kernel0] → [Pod Kernel0 context] → [QEMU User0] → [VM Kernel1] → [App User1]
↑ ↑ ↑ ↑ ↑
Standard CNI veth + TAP reader + Full network Application
Linux stack iptables virtio-net stack in VM
KEY DIFFERENCE:
- Anomaly 1: Nested VM (Windows contains VirtualBox contains Ubuntu)
Two separate kernels: Windows (kernel0) and Ubuntu (kernel1)
Heavier: kernel0 + user0 + kernel1 + user1
- Anomaly 2: VM-in-container (Ubuntu contains Pod contains VM)
SAME KERNEL for host and pod (kernel0 in different netns)
Lighter: kernel0 (host+pod) + user0 (QEMU) + kernel1 (VM) + user1 (app)