3 min read

eBPF Network Vershitifier

eBPF Network Vershitifier
Photo by Ruben Mishchuk / Unsplash

Following on from my previous writings on eBPF, I wanted to build something a bit more useful. Enter the eBPF Network Vershitifier! The vershitifier is a simple ebpf-go application that randomly drops outgoing packets for targeted processes using the eBPF TC filters. This looks like ordinary packet loss to the targeted process, which lets us see how it will react under carefully controlled conditions.

The CLI takes the name of the process to vershitify, and the % of traffic to drop. For instance, to drop 25% of all the traffic ping sends:

> ./vershitifier -interface eth0 -command ping -drop 25
0:00
/0:23

The rest of the post will look at the different eBPF hooks used, with a link at the start of each section to the corresponding code.

Finding Process IDs 📜🔗

Firstly, we need to find all processes that have been launched for a given binary. To do this, we can attach a kprobe and a kretprobe to execve, which is part of the process launching mechanism in the kernel [1]. In the kprobe we see what the parent process looks like, in the kretprobe the launched child.

In the kretprobe, we then use an eBPF helper to get the processes comm, which will contain the name of the process - e.g. curl - with the leading path removed. Once we have this, we can write the bits we've discovered to a map - the process and parent back to our CLI application so it knows we've found a new instance, and pass the PID to the next bit of our eBPF stack.

Identifying Sockets 📜🔗

Now that we know which process IDs to watch, we want to catch their creation of sockets, so we know which traffic we are targeting. Here we're using a cgroup probe, on sock_create. I spent a bunch of time spelunking [2] through potential probes here to try and find one that saw both the source PID and the socket information we might care about - ports and so on. Because we're using a cgroup probe, we need to figure out which cgroup to attach it to [3] which adds some complexity.

When we discover a socket associated with a PID we're tracking, we mark it with a magic number [4]. We can pick up socket marks in the TC filter later on, which makes this a straightforward option, but in situations where other things are chucking marks onto sockets, strange things will happen.

Anyhow - as it stands the code for this bit is quite straightforward:

SEC("cgroup/sock_create")
int create_socket(struct bpf_sock* info) {

	__u32 pid = bpf_get_current_pid_tgid() >> 32;

	// is this process in our targeted process map? If it is, mark
	__u32* val = bpf_map_lookup_elem(&processes_to_track, &pid);
	if (!val || *val == 0) {
		return 1;
	}

    // Mark the socket
	info->mark = 123;

	return 1;
}

Vershitifying Traffic 📜🔗

Finally, we need to actually drop some traffic, this time using an eBPF TC program. The Traffic Control subsystem in Linux lets us muck about with the transmission of packets, and is typically used to do quality-of-service based priority mangling. For vershitification purposes it's a great place to mess about because it "sees" the socket options of the transmitting process - including our socket mark - and it includes the TC_ACT_STOLEN action, which pretends the packet was queued for transmission while actually silently dropping it.

To decide whether or not to drop a packet, we generate a random number between 0 and 100 with bpf_get_prandom_u32() % 100 , then compare that to our drop percentage, which we pass into the program dynamically at load time using RewriteConstants.

Potential Improvements

  • I'm using TC because it's easy to track which traffic is associated with a particular process, but so far we're dropping outgoing traffic. I believe it should be possible on TC ingress too.
  • I don't think I need to use the cgroup-specific probe to discover sockets.

  1. When a process launches another process in Linux, what typically happens is that the program forks itself, copying itself under a new PID, and then uses execve to replace the contents of that process with the process it is launching. A common thing to see would be a shell - 'bash' and friends - in the kprobe - then the process the shell has run in the kretprobe. ↩ī¸Ž

  2. Read: attaching probes and dumping out the contents of all the things passed in to see what they do. ↩ī¸Ž

  3. Code for this here. I suspect that the way i've done this may not work well nested cgroups. ↩ī¸Ž

  4. A socket mark is a uint32 that is carried along on the internal data structures used to track sockets in the Kernel, and can be used for things like making iptables decisions. ↩ī¸Ž