Skip to content

Instantly share code, notes, and snippets.

@yadutaf
Created July 1, 2025 20:16
Show Gist options
  • Save yadutaf/1615ddc0dc7e9a02a4781b872e34c222 to your computer and use it in GitHub Desktop.
Save yadutaf/1615ddc0dc7e9a02a4781b872e34c222 to your computer and use it in GitHub Desktop.
Linux Netkit interface eBPF "Hello World".

This Gist demoes Linux' netkit interface pairs. These interfaces are successors for veth tailor made for eBPF and high performance.

Usage

Create a 'lab' setup:

# Create the 'lab' namespace
sudo ip netns add lab

# Create and setup the interface pair with both sides in blackhole mode
sudo ip link add nk-host type netkit blackhole peer blackhole name nk-container
sudo ip link set nk-container netns lab
sudo ip netns exec lab ip addr add 10.42.0.2/8 dev nk-container
sudo ip netns exec lab ip link set lo up
sudo ip netns exec lab ip link set nk-container up
sudo ip addr add 10.42.0.1/8 dev nk-host
sudo ip link set nk-host up

Build and run:

go mod init hello-netkit
go mod tidy
go get github.com/cilium/ebpf/cmd/bpf2go
go generate && go build && sudo ./hello-netkit

The setup can be tested with a simple ping 10.42.0.2 in the host, with the program running, and without.

Reference

See https://blog.yadutaf.fr/2025/07/01/introduction-to-linux-netkit-interfaces-with-a-grain-of-ebpf/ for the full blog post.

package main
import (
"flag"
"fmt"
"net"
"os"
"os/signal"
"syscall"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -tags linux netkit netkit.c
const (
defaultInterfaceName = "nk-host"
)
func main() {
// Get host interface name from the command line
interfaceName := flag.String("interface", defaultInterfaceName, "Host side netkit interface")
if interfaceName == nil || *interfaceName == "" {
flag.Usage()
}
// Resolve the interface name to an interface index
ifIndex, err := net.InterfaceByName(*interfaceName)
if err != nil {
panic(fmt.Errorf("could not resolve interface %s index: %w", *interfaceName, err))
}
fmt.Printf("Interface %s index is %d\n", *interfaceName, ifIndex.Index)
// Load the programs into the Kernel
collSpec, err := loadNetkit()
if err != nil {
panic(fmt.Errorf("could not load collection spec: %w", err))
}
coll, err := ebpf.NewCollection(collSpec)
if err != nil {
panic(fmt.Errorf("could not load BPF objects from collection spec: %w", err))
}
defer coll.Close()
// Attach the program to the primary interface
primaryLink, err := link.AttachNetkit(link.NetkitOptions{
Program: coll.Programs["netkit_primary"],
Interface: ifIndex.Index,
Attach: ebpf.AttachNetkitPrimary,
})
if err != nil {
panic(fmt.Errorf("could not attach primary prog %w", err))
}
defer primaryLink.Close()
// Attach the program to the peer, directly from the host, via the primary
peerLink, err := link.AttachNetkit(link.NetkitOptions{
Program: coll.Programs["netkit_peer"],
Interface: ifIndex.Index,
Attach: ebpf.AttachNetkitPeer,
})
if err != nil {
panic(fmt.Errorf("could not attach peer prog %w", err))
}
defer peerLink.Close()
// Forwarding is now enabled until exit
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
fmt.Printf("%s is now forwarding IP packets until Ctrl+C is pressed.\n", *interfaceName)
<-done
}
//go:build ignore
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("netkit/primary")
int netkit_primary(struct __sk_buff *skb) {
return TCX_PASS;
}
SEC("netkit/peer")
int netkit_peer(struct __sk_buff *skb) {
return TCX_PASS;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment