Skip to content

Instantly share code, notes, and snippets.

@yadutaf
Last active September 17, 2025 19:41
Show Gist options
  • Select an option

  • Save yadutaf/cdb3f6ceafa0154c59884f9aabf4106c to your computer and use it in GitHub Desktop.

Select an option

Save yadutaf/cdb3f6ceafa0154c59884f9aabf4106c to your computer and use it in GitHub Desktop.
netkit-to-netkit communication with eBPF

This Gist is a companion for the Creating a Linux 'yogurt-phone' — with netkit and a grain of eBPF blog post. It demoes Linux' netkit interface pairs with a local netns-to-netns communication scenario, inspired by "Yogurt Phones". Netkit interfaces are successors for veth tailor made for eBPF and high performance.

Usage

Create a 'lab' setup:

#!/bin/bash
set -euo pipefail

for i in {1..2}
do
    NETNS="yogurt-${i}"
    IFNAME_PREFIX="yg${i}"

    echo "➡️  Creating netns ${NETNS}..."

    # Reset the network namespace
    mountpoint -q "/run/netns/${NETNS}" && sudo ip netns del "${NETNS}"
    sudo ip netns add "${NETNS}"

    # Create and setup the interface pair with both sides in blackhole mode
    sudo ip link add "${IFNAME_PREFIX}-host" type netkit blackhole peer blackhole name "${IFNAME_PREFIX}-cont"
    sudo ip link set "${IFNAME_PREFIX}-cont" netns "${NETNS}"
    sudo ip netns exec "${NETNS}" ip addr add "10.42.0.${i}/24" dev "${IFNAME_PREFIX}-cont"
    sudo ip netns exec "${NETNS}" ip link set lo up
    sudo ip netns exec "${NETNS}" ip link set "${IFNAME_PREFIX}-cont" up
    sudo ip link set "${IFNAME_PREFIX}-host" up
done

echo "All done ✅"
exit 0

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 first netns, with the program running, and without:

sudo ip netns exec yogurt-1 ping 10.42.0.2

Reference

See https://blog.yadutaf.fr/2025/09/16/creating-a-yogurt-phone-with-netkit-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 (
defaultInterface1Name = "yg1-host"
defaultInterface2Name = "yg2-host"
)
type nkeBPFAttachment struct {
primaryLink link.Link
peerLink link.Link
iface *net.Interface
}
func (a *nkeBPFAttachment) GetInterface() *net.Interface {
return a.iface
}
func (a *nkeBPFAttachment) Close() error {
if a.primaryLink != nil {
if err := a.primaryLink.Close(); err != nil {
return err
}
a.primaryLink = nil
}
if a.peerLink != nil {
if err := a.peerLink.Close(); err != nil {
return err
}
a.peerLink = nil
}
return nil
}
func attachToInterface(interfaceName string, coll *ebpf.Collection) (*nkeBPFAttachment, error) {
var err error
a := nkeBPFAttachment{}
// Resolve the interface name to an interface index
a.iface, err = net.InterfaceByName(interfaceName)
if err != nil {
return nil, err
}
fmt.Printf("Interface %s index is %d\n", interfaceName, a.iface.Index)
// Attach the program to the primary interface
a.primaryLink, err = link.AttachNetkit(link.NetkitOptions{
Program: coll.Programs["netkit_primary"],
Interface: a.iface.Index,
Attach: ebpf.AttachNetkitPrimary,
})
if err != nil {
a.Close()
return nil, err
}
// Attach the program to the peer, directly from the host, via the primary
a.peerLink, err = link.AttachNetkit(link.NetkitOptions{
Program: coll.Programs["netkit_peer"],
Interface: a.iface.Index,
Attach: ebpf.AttachNetkitPeer,
})
if err != nil {
a.Close()
return nil, err
}
return &a, nil
}
func main() {
// Get host interface names from the command line
interface1Name := flag.String("interface1", defaultInterface1Name, "First host side netkit interface")
interface2Name := flag.String("interface2", defaultInterface2Name, "Second host side netkit interface")
if interface1Name == nil || *interface1Name == "" {
flag.Usage()
}
if interface2Name == nil || *interface2Name == "" {
flag.Usage()
}
// 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 to the interfaces
link1, err := attachToInterface(*interface1Name, coll)
if err != nil {
panic(fmt.Errorf("could not attach prog to %s: %w", *interface1Name, err))
}
defer link1.Close()
link2, err := attachToInterface(*interface2Name, coll)
if err != nil {
panic(fmt.Errorf("could not attach prog to %s: %w", *interface2Name, err))
}
defer link2.Close()
// Configure the loaded program
coll.Variables["container_1_if_id"].Set(uint32(link1.iface.Index))
coll.Variables["container_2_if_id"].Set(uint32(link2.iface.Index))
// Forwarding is now enabled until exit
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
fmt.Printf("Now forwarding IP packets between %s and %s until Ctrl+C is pressed.\n", *interface1Name, *interface2Name)
<-done
}
//go:build ignore
#include <linux/bpf.h>
#include <linux/if_link.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
// Dynamic configuration (i.e. modifiable at runtime by userspace)
volatile __u32 container_1_if_id = -1;
volatile __u32 container_2_if_id = -1;
// Static configuration
__hidden const __u8 magic_redirect_seen = 0x42;
SEC("netkit/primary")
int netkit_primary(struct __sk_buff *skb) {
// Let the packet flow, if, and only if, it has been seen by the peer
if (skb->cb[0] == magic_redirect_seen) {
skb->cb[0] = 0;
return NETKIT_PASS;
}
return NETKIT_DROP;
}
SEC("netkit/peer")
int netkit_peer(struct __sk_buff *skb) {
__u32 target_interface = 0;
// Route the packet
if (skb->ifindex == container_1_if_id) {
target_interface = container_2_if_id;
} else if (skb->ifindex == container_2_if_id) {
target_interface = container_1_if_id;
} else {
return NETKIT_DROP;
}
// Mark the packet as seen, so that the primary interface knows to forward it
skb->cb[0] = magic_redirect_seen;
// Instruct to kernel to redirect the packet once we're done
bpf_redirect(target_interface, 0);
return NETKIT_REDIRECT;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment