Skip to main content

VPC-CNI Native Routing Details

When installing Cilium on TKE with Native Routing (VPC-CNI), three configurations differ from the Overlay mode:

ConfigNativeOverlayReason
extraConfig.local-router-ipv4169.254.32.16 (explicit)Auto-assignedcilium doesn't manage Pod IPAM
sysctlfix.enabledfalse (must disable)false (disabled) + deploy sysctl override DSsystemd-sysctl restart replays distro defaults
BPF Host Routing data-path hit❌ bypassed✅ hitendpointRoutes bypasses cilium_host

These three configs share a single root cause: Pod IPs are legitimate VPC IPs, so cilium uses endpointRoutes to create per-Pod routes, and packets bypass cilium_host. This is the common characteristic of any solution that hands Pods cloud-provider VPC IPs — including cilium's own native ENI/GKE/Azure IPAM and chained CNI setups like AWS EKS.

This article explains the principles behind each configuration.

Why local-router-ipv4?

The Role of cilium_host

Cilium creates a pair of virtual interfaces on every node:

  • cilium_host: The "gateway" interface on the node, serving as the next-hop for all pods on that node.
  • cilium_net: The veth peer paired with cilium_host.

cilium_host must have an IP address; otherwise node-local routing lacks an "exit".

┌──────────────────────────────────────┐
│ Node │
│ │
│ ┌──────────┐ ┌────────────┐ │
│ │ Pod │─────▶│ cilium_host│──▶│ Egress
│ │ (lxcXX) │ │ (gateway) │ │
│ └──────────┘ └────────────┘ │
└──────────────────────────────────────┘

The Native Mode Special Case

In Native Routing (VPC-CNI) mode, cilium does not manage Pod IP allocation: Pods are attached to ENIs directly, with IPs assigned by VPC-CNI from VPC subnets. Cilium has no knowledge of Pod IP sources.

Since cilium can't determine a suitable IP for cilium_host, the user must explicitly specify one via local-router-ipv4 — an address that won't conflict with any Pod IP.

Why 169.254.32.16?

169.254.0.0/16 is the IPv4 link-local address range (RFC 3927):

  1. Non-routable: Never conflicts with VPC IPs or Service CIDRs.
  2. Uniform across nodes: All nodes can use the same value, simplifying configuration and troubleshooting.
  3. TKE-verified: 169.254.32.16 is confirmed not to be used by other TKE components.
Other uses of 169.254/16 on TKE

TKE also uses the 169.254.0.0/16 range for:

  • Metadata Service (IMDS)
  • apiserver internal VIP (the address returned by kubectl get ep kubernetes)
  • COS / image registry / some internal service VIPs

169.254.32.16 is confirmed not to conflict with any of the above.

Why Overlay Doesn't Need It

In Overlay mode, cilium manages Pod IP allocation (multi-pool IPAM). It knows the node's PodCIDR and automatically assigns a non-conflicting IP to cilium_host from that range — no user intervention needed.

Why Disable sysctlfix?

sysctlfix restarts the host's systemd-sysctl.service on every cilium pod start, replaying all distro sysctl defaults. For Native mode this breaks networking outright: tke-eni-agent sets eth0's rp_filter to 2 (loose) to accommodate VPC-CNI policy routing, and the replay resets it to 1 (strict), dropping asymmetric traffic for auxiliary-ENI IPs.

Native mode just disables sysctlfix — no compensation needed: endpointRoutes provides symmetric per-endpoint routes for every Pod, so the lxc rp_filter value doesn't affect connectivity (Overlay mode is different — its routes aggregate to cilium_host, creating asymmetry that requires the cilium-sysctl-override DaemonSet compensation).

For the full analysis (sysctlfix's two actions, the udev per-interface sysctl replay, the routing symmetry principle, the per-mode decision table and troubleshooting), see sysctlfix and rp_filter Deep Dive.

BPF Host Routing Not Hit in Native Mode

What is Host Routing

Host Routing refers to how packets are forwarded after entering the node's host network namespace. Cilium provides two implementations:

  • Legacy Host Routing: The default. Packets traverse the full Linux network stack — netfilter (iptables) hooks, conntrack, kernel routing table lookup — before being forwarded to the target device. Feature-complete and most compatible, but each hop has overhead.
  • BPF Host Routing: Introduced in cilium 1.9+. Uses tc-bpf programs at the cilium_host device ingress to perform endpoint lookup, service backend lookup, dst MAC rewrite, and redirect to the target device in one shot — completely bypassing netfilter / kernel routing. Higher performance.
┌──────────────────────────────────────────────────────┐
│ Packet forwarding paths on node │
├──────────────────────────────────────────────────────┤
Legacy │ ingress → tc-bpf (policy) → host stack │
│ → netfilter / conntrack → routing table │
│ → veth/eth → out │
├──────────────────────────────────────────────────────┤
BPF host │ ingress → tc-bpf (policy + lookup + redirect) │
routing │ → veth/eth → out (skips netfilter / routing) │
└──────────────────────────────────────────────────────┘

Why BPF host routing is not hit in Native mode

For BPF host routing to actually take effect on the data path, two conditions must hold simultaneously:

Condition 1: cilium-agent does not force fallback at the config layer

cilium-agent checks at startup and force-sets EnableHostLegacyRouting=true (so the ENABLE_HOST_ROUTING macro is not compiled into the BPF program) under either condition (source: pkg/kpr/initializer/kube_proxy_replacement.go:46-64):

  • kubeProxyReplacement=false → fallback
  • enableIPv4Masquerade=true without bpf.masquerade=true (i.e. iptables masquerade) → fallback

Required helm values:

kubeProxyReplacement: true
bpf:
masquerade: true # key switch

When both are satisfied, cilium status reports Host Routing: BPF.

Condition 2: packets actually traverse cilium_host

The BPF host routing code is attached to the tc-bpf program on the cilium_host device (source: ENABLE_HOST_ROUTING macro branch in bpf/bpf_host.c). If packets never reach cilium_host, the code is never executed — even if config-layer requirements are met and cilium status reports BPF, BPF host routing does not actually hit on the data path.

endpointRoutes.enabled=true is exactly this case: each Pod has its own kernel route (ip route pointing directly to the lxc device), so packets bypass cilium_host. The comment in pkg/endpoint/endpoint.go:999-1012 NewDatapathConfiguration() confirms this directly:

"Since routing occurs via endpoint interface directly, BPF program on cilium_host interface is bypassed"

VPC-CNI Native must enable endpointRoutes (because Pod IPs are legitimate VPC IPs and cilium does not own IPAM). The result is that cilium status and the data path diverge: status reports BPF, but the data path does not actually hit it. This is unrelated to the cilium-agent fallback check.

BPF host routing hit status by TKE deployment

DeploymentKey helm valuescilium statusData-path hit
GR + Overlay (vxlan)bpf.masquerade=true + no endpointRoutesBPF✅ Hit
VPC-CNI + Overlaybpf.masquerade=true + no endpointRoutesBPF✅ Hit
VPC-CNI + Native (no SNAT)enableIPv4Masquerade=false + endpointRoutes=trueBPF❌ packets bypass cilium_host
VPC-CNI + Native + ip-masqbpf.masquerade=true + endpointRoutes=trueBPF❌ packets bypass cilium_host
VPC-CNI + Native + Egressbpf.masquerade=true + endpointRoutes=trueBPF❌ packets bypass cilium_host

All three install plans in the cilium.sh script set kubeProxyReplacement=true and none of them trigger the config-level fallback described in the "historical pitfall" above, so cilium status uniformly reports Host Routing: BPF. However, each plan arrives at that state for different reasons:

  • The two Overlay plans: explicitly set bpf.masquerade=true — the standard path (condition 1).
  • Plain Native (SNAT off): enableIPv4Masquerade=false disables masquerading entirely — the fallback check only downgrades when iptables masquerading is enabled, so the condition never fires and BPF is still reported.
  • Native + ip-masq / Egress Gateway: the script adds bpf.masquerade=true when these features are enabled.

In Native mode Host Routing: BPF is the status-level state, not a guarantee that BPF host routing actually hits on the data path — that further depends on whether packets traverse cilium_host.

Historical pitfall: if helm values set enableIPv4Masquerade=true but forget bpf.masquerade=true, cilium logs BPF host routing requires enable-bpf-masquerade. Falling back to legacy host routing. at startup, and cilium status directly reports Host: Legacy, Masquerading: IPTables. This is config-level fallback, distinct from the data-path-bypass issue caused by endpointRoutes.

Verification

The cilium status Host Routing field only reflects cilium-agent's config-layer state (it reports BPF whenever kubeProxyReplacement=true + bpf.masquerade=true). It does not tell you whether the data path actually hits BPF host routing. A VPC-CNI Native cluster will also show Host: BPF in cilium status, but because endpointRoutes makes packets bypass cilium_host, the BPF code is in fact not executed.

To accurately determine whether BPF host routing is hit on the data path, check both:

# Step 1: check cilium-agent config status
kubectl -n kube-system exec ds/cilium -- cilium status | grep -E 'KubeProxyReplacement:|Host Routing:|Masquerading:'
# Config OK:
# KubeProxyReplacement: True
# Host Routing: BPF
# Masquerading: BPF (...)
# Config NOT OK (fallback to legacy):
# Host Routing: Legacy
# Fallback usually means KPR=False or enableIPv4Masquerade is using iptables; check cilium-agent logs:
kubectl -n kube-system logs ds/cilium | grep -iE 'host.legacy|bpf host routing|falling back'

# Step 2: check whether endpointRoutes is enabled — if yes, the BPF program on cilium_host is bypassed
kubectl -n kube-system get cm cilium-config -o jsonpath='{.data.enable-endpoint-routes}'
# Output "true": Pod traffic uses per-endpoint veth routes, BPF host routing is NOT hit
# Output ""/missing: Pod traffic flows through cilium_host, BPF host routing IS hit

Source reference: pkg/endpoint/endpoint.go (v1.20.1) NewDatapathConfiguration comment explicitly states "Since routing occurs via endpoint interface directly, BPF program on cilium_host interface is bypassed".

Performance Impact

When cilium_host is bypassed and Pod traffic flows via per-endpoint veth, each packet incurs the following extra cost:

  • Extra netfilter hooks (PREROUTING / FORWARD / INPUT / OUTPUT / POSTROUTING)
  • Conntrack table lookup and update (the state machine runs even without explicit rules)
  • Kernel routing table lookup (FIB lookup)

Measured (cilium 1.19.5, SA5.LARGE8 4C8G, TencentOS 4, kernel 6.6; environment and full data in the Cilium Network Performance Benchmark):

  • Cross-node single-stream throughput hits 10 Gbps burst ceiling for all three; no throughput difference
  • Cross-node keepalive RPS: Native and Overlay are nearly identical; both are ~13% below iptables (Native pays for cni-chaining + per-endpoint routing; Overlay pays for VXLAN encap/decap; the magnitudes are comparable)
  • Cross-node short-conn RPS: Native and Overlay are also nearly identical
  • TCP_RR p99: Native 103 µs vs Overlay 118 µs vs iptables 107 µs (all three sit within the ~100-120 µs noise band with unstable ordering; sub-millisecond differences are invisible to applications and do not indicate which option is better)
  • HTTP p99 @1000 QPS: 0.99 ms across all three clusters — under realistic application load all three are equivalent

Full data: Cilium Network Performance Benchmark and Cilium Performance Test.

Do not abandon VPC-CNI Native just to get BPF host routing:

  • Native mode's core value is Pod IP = VPC IP — natively recognized by VPC routing, security groups, CLB, and CCN
  • Switching to Overlay does make the BPF program on cilium_host hit, but you lose: direct Pod IP routing externally, L4 LB direct-to-Pod, unified VPC IPAM
  • Measured Native vs Overlay end-to-end RPS/latency differ <1%; the practical gain of "BPF host routing hit" on the cross-node path is minimal

Real reasons to switch to Overlay:

  • Pod IP count exceeds ENI capacity, or you need cross-VPC CIDR reuse (architectural reasons)
  • Node under heavy PPS, nf_conntrack_count approaching nf_conntrack_max (still depends on traffic pattern, not host routing implementation)

Unaffected Capabilities

Even though the BPF program on cilium_host is bypassed, the following cilium core capabilities work normally across all deployment modes:

  • L3/L4/L7 NetworkPolicy: BPF programs are attached to lxc device ingress/egress hooks (decoupled from host routing)
  • Hubble Observability: Same as above; flow collection uses lxc BPF programs
  • kubeProxyReplacement: Fully replaces kube-proxy (ClusterIP / NodePort / HostPort forwarding)
  • CiliumLocalRedirectPolicy: Node-local DNS cache and similar scenarios
  • Egress Gateway: Available; see Egress Gateway Practices

Cross-Cloud Comparison: All Cloud-Provider Native IPAM is the Same

Cilium's official helm chart automatically sets enable-endpoint-routes: "true" when eni.enabled=true (cilium managing AWS ENI IPAM):

# install/kubernetes/cilium/templates/cilium-configmap.yaml (v1.20.1)
{{- if .Values.eni.enabled }}
{{- if not .Values.endpointRoutes.enabled }}
enable-endpoint-routes: "true"
{{- end }}

GKE configuration (gke.enabled=true) also auto-enables endpoint-routes (source: Documentation/network/concepts/routing.rst).

The reason is the same as TKE Native: whenever Pod IPs are legitimate cloud-provider VPC IPs (whether via cni-chaining or cilium native IPAM), cilium does not own the IP source and should not route everything via cilium_host, so it uses endpointRoutes for per-Pod routes — but this also makes the BPF program on cilium_host useless.

SolutionIPAMendpointRoutesPod IP typeBPF host routing hit
TKE VPC-CNI + Native (cni-chaining)tke-eni-ipamdRequired true (manual)VPC IP
AWS EKS + cilium ENI IPAM (non-chained)cilium eniAuto true (chart)VPC IP
AWS EKS + chained aws-cniaws-vpc-cniRequired true (manual)VPC IP
GKE + cilium GKE modek8s host-scopeAuto true (chart)Alias IP (VPC)
Self-hosted Cilium Native + Cluster Pool IPAMcluster-poolDefault falseCilium-managed CIDR
TKE Cilium Overlaymulti-poolDefault falseCilium-managed CIDR

The last two rows are the counter-examples — when cilium fully owns Pod CIDR (Native or Overlay), endpointRoutes is not forced on, and the BPF program on cilium_host hits on the data path. So the real determinant of "BPF host routing hit or miss" is not the routing mode (Native/Tunnel) and not whether cni-chaining is used, but whether Pod IPs come from cloud-provider VPC.

References