Skip to content

fix(encapsulation): use cilium_tunl for IPIP tunnel in Cilium mode#8

Merged
kvaps merged 1 commit intomainfrom
fix/cilium-tunl-ipip
Feb 16, 2026
Merged

fix(encapsulation): use cilium_tunl for IPIP tunnel in Cilium mode#8
kvaps merged 1 commit intomainfrom
fix/cilium-tunl-ipip

Conversation

@kvaps
Copy link
Member

@kvaps kvaps commented Feb 16, 2026

Summary

  • Use cilium_tunl instead of cilium_ipip4 for IPIP encapsulation in Cilium compatibility mode
  • When Cilium's enable-ipip-termination is active, cilium_ipip4 is a receive-only device (DSR) that cannot transmit packets. cilium_tunl (the renamed tunl0) supports both TX and RX
  • If cilium_tunl already exists (managed by Cilium), reuse it; otherwise create it as a named IPIP tunnel
  • Add NewIPIPWithName helper to support creating IPIP tunnels with custom names

Test plan

  • Verified cilium_ipip4 shows TX errors (0 packets sent, thousands of errors)
  • Verified cilium_tunl transmits successfully (both TX and RX counters incrementing)
  • Tested kubectl exec on worker nodes through IPIP+VxLAN path — works with cilium_tunl
  • All kilo pods running with 0 restarts across control plane and worker nodes

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved tunnel lifecycle management to detect and reuse existing kernel tunnel interfaces, preventing unnecessary recreation and configuration conflicts.
  • New Features

    • Added support for creating IPIP tunnel interfaces with custom names for enhanced network configuration flexibility.

When Cilium's enable-ipip-termination is active, it renames the
kernel's tunl0 to cilium_tunl and creates a receive-only cilium_ipip4
device for DSR. The cilium_ipip4 interface cannot transmit packets
(TX errors), so use cilium_tunl which supports both TX and RX.

If cilium_tunl already exists (Cilium manages it), reuse it. Otherwise,
create it so the interface name is consistent regardless of whether
enable-ipip-termination is active.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

The changes introduce conditional tunnel ownership handling to Cilium's encapsulation module. Cilium now detects existing kernel cilium_tunl tunnels via netlink and reuses them if available, otherwise creates a new IPIP tunnel. The iproute module is refactored to support parameterized tunnel names through a new NewIPIPWithName function.

Changes

Cohort / File(s) Summary
Cilium Tunnel Ownership
pkg/encapsulation/cilium.go
Adds netlink integration and ownsTunnel flag to track tunnel ownership. Init logic now checks for existing kernel tunnel via netlink; if found, reuses it with ownsTunnel=false; otherwise creates new IPIP interface with ownsTunnel=true. CleanUp conditionally removes tunnel only if owned by cilium.
IPIP Tunnel Parameterization
pkg/iproute/ipip.go
Introduces DefaultTunnelName constant and new NewIPIPWithName(baseIndex, name) function to support creating IPIP tunnels with custom names. Existing NewIPIP delegates to new function with default name.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A tunnel so clever, it checks what's below,
If one's already there, let the kernel now know!
Or craft a fresh path with a name of its own,
Sharing the way through each bit and each stone. 🐰✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (3 files):

⚔️ pkg/encapsulation/cilium.go (content)
⚔️ pkg/iproute/ipip.go (content)
⚔️ pkg/mesh/routes.go (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: using cilium_tunl instead of cilium_ipip4 for IPIP tunnel in Cilium mode, which aligns with the primary objective and code modifications across both affected files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/cilium-tunl-ipip
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch fix/cilium-tunl-ipip
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @kvaps, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves an issue with IPIP tunnel encapsulation in Cilium compatibility mode by ensuring that the correct network interface, cilium_tunl, is used for both sending and receiving encapsulated traffic. Previously, cilium_ipip4 was used, which is a receive-only device, leading to transmission failures. The changes include logic to intelligently manage the cilium_tunl interface, either reusing an existing one or creating a new named tunnel, and introduces a new utility for flexible IPIP tunnel creation.

Highlights

  • IPIP Encapsulation Fix: Switched from cilium_ipip4 to cilium_tunl for IPIP encapsulation in Cilium compatibility mode to ensure bidirectional traffic flow.
  • Cilium enable-ipip-termination Compatibility: Addressed the limitation of cilium_ipip4 being a receive-only device by utilizing cilium_tunl, which supports both transmit and receive operations.
  • Dynamic Tunnel Management: Implemented logic to detect and reuse an existing cilium_tunl interface managed by Cilium, or create a new one if not present.
  • Flexible IPIP Tunnel Creation: Introduced a new helper function, NewIPIPWithName, allowing the creation of IPIP tunnels with custom, user-defined names.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • pkg/encapsulation/cilium.go
    • Added ciliumTunlIface constant for the cilium_tunl interface name.
    • Introduced an ownsTunnel boolean field to the cilium struct to track tunnel ownership.
    • Modified the CleanUp method to only remove IPIP devices if the cilium instance is responsible for their creation.
    • Updated the Init method to first check for an existing cilium_tunl interface and, if found, reuse it; otherwise, it creates a new named IPIP tunnel using cilium_tunl as its name.
  • pkg/iproute/ipip.go
    • Renamed the tunnelName constant to DefaultTunnelName.
    • Refactored the NewIPIP function to delegate its logic to the newly introduced NewIPIPWithName function, using DefaultTunnelName.
    • Added a new public function NewIPIPWithName that allows creating an IPIP tunnel with a specified name, rather than always defaulting to tunl0.
Activity
  • Verified that cilium_ipip4 exhibited transmit errors, confirming the problem.
  • Confirmed that cilium_tunl successfully transmits and receives packets, validating the solution.
  • Successfully tested kubectl exec functionality over the IPIP+VxLAN path with cilium_tunl.
  • Ensured all Kilo pods remained stable with no restarts across control plane and worker nodes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@kvaps kvaps marked this pull request as ready for review February 16, 2026 13:50
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request correctly addresses the issue where cilium_ipip4 was unable to transmit packets in Cilium mode by switching to cilium_tunl. It also introduces a necessary check for interface ownership to prevent Kilo from destructively cleaning up interfaces managed by Cilium. However, there is a significant bug in the Init method of the cilium encapsulator: when an existing interface is reused, the code returns early, skipping critical configuration steps such as setting the MTU and ensuring the interface is administratively UP. This could lead to connectivity issues or suboptimal performance.

Comment on lines +100 to 114
if link, err := netlink.LinkByName(ciliumTunlIface); err == nil {
c.iface = link.Attrs().Index
c.ownsTunnel = false
return nil
}
iface, err := iproute.NewIPIPWithName(base, ciliumTunlIface)
if err != nil {
return fmt.Errorf("failed to create tunnel interface: %v", err)
}
if err := iproute.Set(iface, true); err != nil {
return fmt.Errorf("failed to set tunnel interface up: %v", err)
}
c.iface = iface
c.ownsTunnel = true
return nil

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of Init returns early if the cilium_tunl interface already exists. This bypasses the MTU configuration (performed inside NewIPIPWithName) and the call to iproute.Set(iface, true), which ensures the interface is UP. Even when reusing an existing interface, Kilo should ensure it is correctly configured for its use case.

Additionally, the ownership check can be integrated more cleanly to ensure the rest of the initialization logic always runs.

	_, err := netlink.LinkByName(ciliumTunlIface)
	c.ownsTunnel = err != nil
	iface, err := iproute.NewIPIPWithName(base, ciliumTunlIface)
	if err != nil {
		return fmt.Errorf("failed to create tunnel interface: %v", err)
	}
	if err := iproute.Set(iface, true); err != nil {
		return fmt.Errorf("failed to set tunnel interface up: %v", err)
	}
	c.iface = iface
	return nil

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/encapsulation/cilium.go (1)

48-57: ⚠️ Potential issue | 🟠 Major

Clean up addresses even when reusing a Cilium-owned tunnel.

When Kilo reuses an existing cilium_tunl interface (ownsTunnel is false), Set() still adds an IP address to the interface. However, CleanUp() returns immediately without calling iproute.DeleteAddresses(), leaving Kilo-assigned addresses on the shared tunnel after shutdown. This causes a resource leak and could lead to routing conflicts on restart.

The fix is to always clean up addresses, but skip interface removal only when the tunnel is not owned:

Proposed fix
 func (c *cilium) CleanUp() error {
-	if !c.ownsTunnel {
-		return nil
-	}
+	if !c.ownsTunnel {
+		return iproute.DeleteAddresses(c.iface)
+	}
 	if err := iproute.DeleteAddresses(c.iface); err != nil {
 		return err
 	}
 	return iproute.RemoveInterface(c.iface)
 }
🤖 Fix all issues with AI agents
In `@pkg/encapsulation/cilium.go`:
- Around line 99-114: When Init finds an existing cilium tunnel (the branch that
sees netlink.LinkByName(ciliumTunlIface)), also bring the link up and ensure its
MTU matches base-20 before returning: set c.iface as you do now, then call the
same helpers used in the creation path (iproute.Set to bring the link up and the
appropriate iproute MTU setter to set MTU = base - 20), and only then set
c.ownsTunnel=false and return; reuse ciliumTunlIface, Init, iproute.Set and the
MTU-setting helper names to locate where to add these calls.
🧹 Nitpick comments (1)
pkg/iproute/ipip.go (1)

39-55: Potential issue: name parameter is not sanitized before passing to shell command.

Line 43 passes name directly into exec.Command("ip", "tunnel", "add", name, ...). Since exec.Command uses argument-based invocation (not shell expansion), this is safe from injection. However, invalid interface names (e.g., names exceeding IFNAMSIZ of 15 chars, or containing /) would produce confusing errors. Currently all callers pass compile-time constants (DefaultTunnelName, ciliumTunlIface), so the practical risk is negligible.

Comment on lines 99 to 114
func (c *cilium) Init(base int) error {
iface, err := iproute.NewIPIP(base)
if link, err := netlink.LinkByName(ciliumTunlIface); err == nil {
c.iface = link.Attrs().Index
c.ownsTunnel = false
return nil
}
iface, err := iproute.NewIPIPWithName(base, ciliumTunlIface)
if err != nil {
return fmt.Errorf("failed to create tunnel interface: %v", err)
}
if err := iproute.Set(iface, true); err != nil {
return fmt.Errorf("failed to set tunnel interface up: %v", err)
}
c.iface = iface
c.ownsTunnel = true
return nil
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reused tunnel skips MTU adjustment and link-up.

When cilium_tunl already exists (lines 100–104), Init sets c.iface and returns immediately without:

  1. Ensuring the link is administratively up (iproute.Set(iface, true)).
  2. Setting the MTU to base MTU − 20.

If Cilium has already configured and brought up the tunnel, this is fine in practice. However, if the MTU of the base interface differs from what Cilium assumed, the tunnel MTU could be mismatched, leading to packet drops or fragmentation on the IPIP path.

Consider at minimum setting the MTU on the reused tunnel to stay consistent with the creation path.

Suggested enhancement
 func (c *cilium) Init(base int) error {
 	if link, err := netlink.LinkByName(ciliumTunlIface); err == nil {
 		c.iface = link.Attrs().Index
 		c.ownsTunnel = false
+		// Ensure the MTU matches what Kilo expects.
+		baseLink, err := netlink.LinkByIndex(base)
+		if err != nil {
+			return fmt.Errorf("failed to get base device: %v", err)
+		}
+		mtu := baseLink.Attrs().MTU - 20
+		if err := netlink.LinkSetMTU(link, mtu); err != nil {
+			return fmt.Errorf("failed to set tunnel MTU: %v", err)
+		}
 		return nil
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *cilium) Init(base int) error {
iface, err := iproute.NewIPIP(base)
if link, err := netlink.LinkByName(ciliumTunlIface); err == nil {
c.iface = link.Attrs().Index
c.ownsTunnel = false
return nil
}
iface, err := iproute.NewIPIPWithName(base, ciliumTunlIface)
if err != nil {
return fmt.Errorf("failed to create tunnel interface: %v", err)
}
if err := iproute.Set(iface, true); err != nil {
return fmt.Errorf("failed to set tunnel interface up: %v", err)
}
c.iface = iface
c.ownsTunnel = true
return nil
func (c *cilium) Init(base int) error {
if link, err := netlink.LinkByName(ciliumTunlIface); err == nil {
c.iface = link.Attrs().Index
c.ownsTunnel = false
// Ensure the MTU matches what Kilo expects.
baseLink, err := netlink.LinkByIndex(base)
if err != nil {
return fmt.Errorf("failed to get base device: %v", err)
}
mtu := baseLink.Attrs().MTU - 20
if err := netlink.LinkSetMTU(link, mtu); err != nil {
return fmt.Errorf("failed to set tunnel MTU: %v", err)
}
return nil
}
iface, err := iproute.NewIPIPWithName(base, ciliumTunlIface)
if err != nil {
return fmt.Errorf("failed to create tunnel interface: %v", err)
}
if err := iproute.Set(iface, true); err != nil {
return fmt.Errorf("failed to set tunnel interface up: %v", err)
}
c.iface = iface
c.ownsTunnel = true
return nil
🤖 Prompt for AI Agents
In `@pkg/encapsulation/cilium.go` around lines 99 - 114, When Init finds an
existing cilium tunnel (the branch that sees
netlink.LinkByName(ciliumTunlIface)), also bring the link up and ensure its MTU
matches base-20 before returning: set c.iface as you do now, then call the same
helpers used in the creation path (iproute.Set to bring the link up and the
appropriate iproute MTU setter to set MTU = base - 20), and only then set
c.ownsTunnel=false and return; reuse ciliumTunlIface, Init, iproute.Set and the
MTU-setting helper names to locate where to add these calls.

@kvaps kvaps merged commit c373706 into main Feb 16, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant