A transparent proxy intercepts and redirects client requests without modifying them or requiring client-side network adjustments. It operates completely invisibly to applications and users, performing tasks such as content filtering, caching, traffic control, and load balancing.
In this newsletter, I’ll walk you through the implementation of transparent proxy with eBPF. Specifically, we’ll utilize Golang alongside the ebpf-go package.
eBPF-accelerated Transparent Proxy
The implementation of a transparent proxy using eBPF involves three distinct eBPF programs, each responsible for different task of network interception and forwarding:
Address Replacement at Connection Establishment: The first eBPF program,
cgroup/connect4
, attaches to theconnect
system call. When a client attempts to connect to a target server, this program intercepts the connection attempt, replacing the destination's IP address and port with those of the local transparent proxy (127.0.0.1, loopback IP) — this redirection is completely invisible to the client. Simultaneously, the original destination address and port are stored within amap_socks
eBPF map, enabling thecgroup/getsockopt
eBPF programs to reference this information later (on the packet’s way back).Source Address Recording Post-Connection: The second eBPF program,
sockops
, executes after a connection between the client and the (transparent) proxy is successfully established. Its primary function is to record the source port and socket cookie mapping in themap_ports
eBPF map. This intermediate step is necessary because the source port is only known after the connection is established.
💡 A socket cookie is a unique identifier assigned to a socket by the kernel, used to track and distinguish individual network connections in eBPF programs.
Forwarding Based on Original Destination Information: The third eBPF program,
cgroup/getsockopt
, is triggered after the proxy inspects the content of the packet and queries for the original destination information using thegetsockopt
call. This program retrieves the original socket's cookie frommap_ports
using the source port and then accesses the original destination information stored inmap_socks
using that cookie. With this information, it is able to establish a connection with the original destination server and forward the client's request.
The following image depicts the entire setup, with each color representing a different stage. They execute in the following order:
🔴 Red -> 🟢 Green -> 🟣 Purple -> 🌸 Pink
All three programs are linked to a specific cgroup. This ensures they’re only activated when processes within this group execute the designated system calls.
In theory, that’s all. Let’s see some code and tests.
Code Example
I find code example renders in Substack tedious, so I’ll refer to my GitHub repository with the code and test results.
Here’s the link.
💡 Hint: I’ve added some useful code comments for you to check — as always :)
Performance Evaluation
I also conducted tests involving 10,000 requests and calculated the average overhead in terms of latency and CPU usage.
The results indicate that the eBPF programs add a constant eBPF overhead of approximately 1ms on average. Additionally, the average CPU load introduced by each hook is as follows:
0.4% for
sockops
0.1% for
cgroup/connect4
, and0.09% for
cgroup/getsockopt
Based on these findings, eBPF proves to be an excellent fit for transparent proxies.
⏪ Did you miss the previous issues? I'm sure you wouldn't, but JUST in case:
I hope you find this resource as enlightening as I did. Stay tuned for more exciting developments and updates in the world of eBPF in next week's newsletter.
Until then, keep 🐝-ing!
Warm regards, Teodor