<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[All Hail The Topology]]></title><description><![CDATA[ Low-level Linux systems and networking, explored through the source.]]></description><link>https://allhailthetopology.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!VPdD!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2b99c97-588d-41c9-b59a-034c6467f8be_571x571.png</url><title>All Hail The Topology</title><link>https://allhailthetopology.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 15 Jul 2026 16:57:14 GMT</lastBuildDate><atom:link href="https://allhailthetopology.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[NetRunner]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[allhailthetopology@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[allhailthetopology@substack.com]]></itunes:email><itunes:name><![CDATA[AllHailTheTopology]]></itunes:name></itunes:owner><itunes:author><![CDATA[AllHailTheTopology]]></itunes:author><googleplay:owner><![CDATA[allhailthetopology@substack.com]]></googleplay:owner><googleplay:email><![CDATA[allhailthetopology@substack.com]]></googleplay:email><googleplay:author><![CDATA[AllHailTheTopology]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Linux transparent proxy internals]]></title><description><![CDATA[Learn how Linux's transparent proxying works by building a toy proxy server in C.]]></description><link>https://allhailthetopology.substack.com/p/linux-transparent-proxy-internals</link><guid isPermaLink="false">https://allhailthetopology.substack.com/p/linux-transparent-proxy-internals</guid><dc:creator><![CDATA[AllHailTheTopology]]></dc:creator><pubDate>Mon, 13 Jul 2026 19:17:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VPdD!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2b99c97-588d-41c9-b59a-034c6467f8be_571x571.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've always thought the transparent proxying functionality in the Linux kernel is one of the coolest but hardest to understand.</p><p>This may have to do with the sheer lack of documentation on the subject or maybe the fact that configuring it requires quite a few moving pieces like IP rules, socket options, and routing.</p><p>Let's explore how transparent proxying works and build a toy transparent proxy server to drive the concepts home.</p><h2>What is transparent proxying?</h2><p>Most people will have colloquial familiarity with the term "proxy" as a networking infrastructure.</p><p>A proxy sits between a client, the source, and a server, the destination, and manages the connections and data between them.</p><p>If this is a TCP proxy, it will terminate the client's connection, holding a socket to it, and will connect to the server, holding a socket for this "leg" as well.</p><p>With both sockets in-hand the proxy will pipe the data between them, performing whatever actions on the data is required.</p><p>Proxies can do all sorts of things, but we just need to understand the fundamentals right now.</p><p></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                      &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;                      
                      &#9474; 66.66.66.66 &#9474;                      
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   conn A   &#9474;    proxy    &#9474;   conn B   &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;10.0.5.1&#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;             &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;10.0.6.1&#9474;
&#9474; client &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508;  terminates &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; server &#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;            &#9474;     and     &#9474;            &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;
                      &#9474;   forwards  &#9474;                      
                      &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;                      
</code></pre></div><p>      </p><p>The above is what, I think, most people imagine when they think 'proxy', though technically, this is a reverse proxy where the proxy sits in front of the server.</p><p>To understand what a <strong>transparent</strong> proxy is they must think about what IP addresses the client will use to connect to the server, and the client IPs seen by the server.<br></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                            &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;                               
                            &#9474; 66.66.66.66 &#9474;                               
      &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   conn A   &#9474;    proxy    &#9474;   conn B   &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;         
      &#9474;10.0.5.1&#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;             &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;10.0.6.1&#9474;         
      &#9474; client &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508;  terminates &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; server &#9474;         
      &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;            &#9474;     and     &#9474;            &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;         
&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   &#9474;   forwards  &#9474;      &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
&#9474;       Conn A          &#9474;   &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;      &#9474;       Conn B          &#9474;
&#9474;10.0.5.1 -&gt; 66.66.66.66&#9474;                        &#9474;66.66.66.66 -&gt; 10.0.6.1&#9474;
&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;                        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</code></pre></div><p></p><p>Let's use the <strong>non-transparent</strong> proxy example above, both the client connection and the server's connections are well aware a proxy server sits between them.</p><p>A transparent proxy is fundamentally different.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                              &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;                                 
                              &#9474; 66.66.66.66 &#9474;                                 
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   conn A   &#9474;    proxy    &#9474;   conn B   &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;           
        &#9474;10.0.5.1&#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;             &#9500;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9658;10.0.6.1&#9474;           
        &#9474; client &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508;  terminates &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9508; server &#9474;           
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;            &#9474;     and     &#9474;            &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;           
  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;   &#9474;   forwards  &#9474;      &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  
  &#9474;       Conn A          &#9474;   &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;      &#9474;       Conn B          &#9474;  
  &#9474; 10.0.5.1 -&gt; 10.0.6.1  &#9474;                        &#9474; 10.0.5.1 -&gt; 10.0.6.1  &#9474;  
  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;                        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  </code></pre></div><p></p><p>Notice, in the <strong>transparent</strong> case the client perceives itself connected directly to the server and the server perceives itself connected directly to the client.</p><p>Both client and server maintain their IP addresses, across both "legs" of the proxy.</p><p>The example above is sometimes called a "fully transparent proxy" since the source IP of the client is maintained from the server's perspective.
It's not uncommon for the server to see the proxy's original IP when, for example, routing the client's IP back to the proxy is difficult, among other reasons.</p><p>While the explanation is simple, like a lot of things with networking, the devil's in the details.</p><p>Let's start building a proxy server which we will modify into a transparent socket as we explain how we can accomplish the above.</p><h2>The topology</h2><p>We need a small testing environment which creates separate layer 3 networks for the client, proxy, and server.</p><p>We can do this with just Linux network namespaces, the <code>ip</code> tool, and <code>veths</code>.</p><p></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"> &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; 
 &#9474; client ns &#9474;        &#9474;   proxy ns   &#9474;        &#9474;  server ns   &#9474; 
 &#9474;      &#9484;&#9472;&#9472;&#9472;&#9472;&#9488;        &#9484;&#9472;&#9472;&#9472;&#9472;&#9488;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9488;        &#9484;&#9472;&#9472;&#9472;&#9472;&#9488;         &#9474; 
 &#9474;      &#9474;    &#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;    &#9474;    &#9474;    &#9532;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;    &#9474;         &#9474; 
 &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9524;&#9472;&#9472;&#9472;&#9472;&#9496;        &#9492;&#9472;&#9472;&#9472;&#9472;&#9496;&#9472;&#9472;&#9472;&#9472;&#9492;&#9472;&#9472;&#9472;&#9472;&#9496;        &#9492;&#9472;&#9472;&#9472;&#9472;&#9496;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; 
      10.0.5.1      10.0.5.10   10.0.6.10     10.0.6.1         </code></pre></div><p></p><p>The above diagram illustrates the topology, three network namespaces connected by <code>veths</code>.</p><p><code>Makefile<br><br></code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">LINK = ip link
NETNS = ip netns
NETNS_EXEC = ip netns exec
NETNS_CLIENT = client
NETNS_PROXY = proxy
NETNS_SERVER = server

topology:
&#9;# create network namespaces
&#9;$(NETNS) add $(NETNS_CLIENT)
&#9;$(NETNS) add $(NETNS_PROXY)
&#9;$(NETNS) add $(NETNS_SERVER)

&#9;# wire them together with veths
&#9;$(LINK) add name client-a type veth peer name proxy-a
&#9;$(LINK) set dev client-a netns $(NETNS_CLIENT)
&#9;$(LINK) set dev proxy-a netns $(NETNS_PROXY)

&#9;$(LINK) add name proxy-b type veth peer name server-a
&#9;$(LINK) set dev proxy-b netns $(NETNS_PROXY)
&#9;$(LINK) set dev server-a netns $(NETNS_SERVER)

&#9;# configure IP networking
&#9;$(NETNS_EXEC) $(NETNS_CLIENT) ip addr add 10.0.5.1/24 dev client-a
&#9;$(NETNS_EXEC) $(NETNS_CLIENT) ip link set dev client-a up
&#9;$(NETNS_EXEC) $(NETNS_CLIENT) ip route add default via 10.0.5.10

&#9;$(NETNS_EXEC) $(NETNS_PROXY) ip addr add 10.0.5.10/24 dev proxy-a
&#9;$(NETNS_EXEC) $(NETNS_PROXY) ip link set dev proxy-a up

&#9;$(NETNS_EXEC) $(NETNS_PROXY) ip addr add 10.0.6.10/24 dev proxy-b
&#9;$(NETNS_EXEC) $(NETNS_PROXY) ip link set dev proxy-b up

&#9;$(NETNS_EXEC) $(NETNS_SERVER) ip addr add 10.0.6.1/24 dev server-a
&#9;$(NETNS_EXEC) $(NETNS_SERVER) ip link set dev server-a up
&#9;$(NETNS_EXEC) $(NETNS_SERVER) ip route add default via 10.0.6.10

&#9;# enable arp proxy in proxy network namespace
&#9;$(NETNS_EXEC) $(NETNS_PROXY) sysctl -w net.ipv4.ip_forward=1
&#9;$(NETNS_EXEC) $(NETNS_PROXY) sysctl -w net.ipv4.conf.all.proxy_arp=1

topology-destroy:
&#9;$(NETNS) del $(NETNS_CLIENT)
&#9;$(NETNS) del $(NETNS_PROXY)
&#9;$(NETNS) del $(NETNS_SERVER)</code></pre></div><p></p><p>We can now test end-to-end connectivity with the command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">$ make topology 
$ sudo ip netns exec client ping 10.0.6.1</code></pre></div><p>This will ping from the client's IP network stack to the server's using the proxy network namespace as a router; it should succeed.</p><h2>A non-transparent proxy server</h2><p>Let's first build a non-transparent proxy server as a stepping stone.</p><p>This transparent proxy will be very dumb, simply to demonstrate the transparent proxying requirements.</p><p>It will only accept one connection at a time, wait for the client to write, return the response from the server back to the client, and then close both the client and server connections.</p><p><code>proxy.c</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">#define _GNU_SOURCE
#include &lt;arpa/inet.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;unistd.h&gt;

#define PROXY_PORT 6666
#define PROXY_TARGET_PORT 8080
#define PROXY_TARGET_IP 0x0a000601
#define MAX_MSG_SIZE 1024 * 1024

int proxy_do(int client_sock) {
    struct sockaddr_in server_addr = {.sin_addr = htonl(PROXY_TARGET_IP),
                                      .sin_port = htons(PROXY_TARGET_PORT),
                                      .sin_family = AF_INET};

    int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    int pipe[2];
    pipe2(pipe, O_NONBLOCK);

    if (server_sock &lt; 0) {
        perror("proxy_do: socket");
        return 0;
    }

    if (connect(server_sock, (struct sockaddr*)&amp;server_addr,
                sizeof(server_addr)) &lt; 0) {
        perror("proxy_do: connect");
        goto cleanup;
    }

    // client -&gt; server via splice pipe
    if (splice(client_sock, 0, pipe[1], 0, MAX_MSG_SIZE, SPLICE_F_MOVE) &lt; 0) {
        perror("proxy_do: client splice to pipe");
        goto cleanup;
    }
    if (splice(pipe[0], 0, server_sock, 0, MAX_MSG_SIZE, SPLICE_F_MOVE) &lt; 0) {
        perror("proxy_do: pipe splice to server");
        goto cleanup;
    }

    // client &lt;- server via splice pipe
    if (splice(server_sock, 0, pipe[1], 0, MAX_MSG_SIZE, SPLICE_F_MOVE) &lt; 0) {
        perror("proxy_do: server splice");
        goto cleanup;
    }
    if (splice(pipe[0], 0, client_sock, 0, MAX_MSG_SIZE, SPLICE_F_MOVE) &lt; 0) {
        perror("proxy_do: pipe splice to client");
        goto cleanup;
    }

cleanup:
    close(server_sock);
    close(pipe[0]);
    close(pipe[1]);
    return 0;
}

int main(int argc, char* argv[]) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock &lt; 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct sockaddr_in addr = {
        .sin_addr = 0, .sin_port = htons(PROXY_PORT), .sin_family = AF_INET};
    if (bind(sock, (struct sockaddr*)&amp;addr, sizeof(addr)) &lt; 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(sock, 1) &lt; 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    printf("proxy start *:%d \n", PROXY_PORT);

    while (1) {
        struct sockaddr_in client = {0};
        socklen_t addr_n = sizeof(client);
        char buf[INET_ADDRSTRLEN];

        int client_sock = accept(sock, (struct sockaddr*)&amp;client, &amp;addr_n);
        if (client_sock &lt; 0) {
            perror("accept");
            continue;
        }

        if (inet_ntop(AF_INET, &amp;client.sin_addr, buf, sizeof(buf)) == NULL) {
            perror("inet_ntop");
            continue;
        }

        printf("new client: %s\n", buf);
        proxy_do(client_sock);
        close(client_sock);
    }
}</code></pre></div><p>Let's also update the <code>Makefile</code> to build the proxy server.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">diff --git a/Makefile b/Makefile
index 217ff37..ef27ffb 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,8 @@ NETNS_CLIENT = client
 NETNS_PROXY = proxy
 NETNS_SERVER = server

+proxy: proxy.o
+
 topology:
        # create network namespaces
        $(NETNS) add $(NETNS_CLIENT)</code></pre></div><p>Run <code>make</code> to build the <code>proxy</code> binary.</p><p>At this point we can test the proxy server in a few terminals.</p><p><code>term1</code>: deploy topology, start an echo server using <code>nc</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">$ sudo make topology
$ sudo ip netns exec server ncat -v -l -k -p 8080 --exec /usr/bin/cat
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::8080
Ncat: Listening on 0.0.0.0:8080</code></pre></div><p><code>term2</code>: start the proxy</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">$ sudo ip netns exec proxy ./proxy
proxy start *:6666</code></pre></div><p><code>term3</code>: issue a request to the proxy using <code>nc</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">$ sudo ip netns exec client bash -c 'echo "all hail the topology!" | nc 10.0.6.10 6666'
all hail the topology!</code></pre></div><p>Once the action in <code>term3</code> is completed we should see the following new lines in <code>term1</code> and <code>term2</code></p><p><code>term1</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">Ncat: Connection from 10.0.6.10.
Ncat: Connection from 10.0.6.10:53918.</code></pre></div><p><code>term2</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">new client: 10.0.5.1</code></pre></div><p>We just built a very dumb, yet functional, proxy server which terminates the client connection, opens a new connection to the server, and streams the bits between both client and server sockets.</p><p>Notice, from the server's perspective, the <strong>proxy</strong> is connecting to it.</p><p>Additionally, the client connects to the <strong>proxy</strong>, not to the server.</p><p>Recall, in the transparent proxying case, we want the client to connect to the proxy using the echo server's address and port, and we want the proxy to connect to the echo server using the original client's address and port.</p><h2>The obstacles</h2><p>For me, it was always easiest to understand how transparent proxying works by first outlining the obstacles it presents in the normal Linux network stack.</p><p>Let's start at the client who we want to send a request directly to the server <code>10.0.6.1:8080</code>.</p><p>We can do this right now, since we have end-to-end connectivity, and it will work but we bypass the proxy altogether, not what we want.</p><p>Consider the current network path for this <code>10.0.6.1:8080</code> packet sent from a client which bypasses the proxy:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;        
        &#9474;proxy-a (veth)&#9474;        
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;        
               &#9474;                
  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9660;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  
  &#9474;         layer 2          &#9474;  
  &#9474; __netif_receive_skb_core &#9474;  
  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  
               &#9474;                
  &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9660;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;  
  &#9474;         layer 3          &#9474;  
  &#9474;         ip_rcv           &#9474;  
  &#9474;    ip_rcv_finish_core    &#9474;  
  &#9474;   ip_route_input_noref   &#9474;  
  &#9474;        ip_forward        &#9474;  
  &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;  
               &#9474;                
        &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9660;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;        
        &#9474;proxy-b (veth)&#9474;        
        &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;        </code></pre></div><p>The packet is received on <code>proxy-a</code> <code>veth</code> inside the proxy network namespace.</p><p>The primary layer 2 function handles the packet, determines it's an IPv4 protocol packet, and calls into <code>ip_rcv</code>.</p><p><code>ip_rcv</code> is the ingress IP packet handling function which does a bit of prep work and quickly calls into <code>ip_rcv_finish_core</code>.</p><p>In the core function a route lookup is done on the packet, this "host" doesn't have the <code>10.0.6.1</code> IP assigned to any of its interfaces, but it <strong>does</strong> have a route to it found by calling <code>ip_route_input_noref</code>.</p><p>Because the kernel has determined it can be routed the <code>ip_forward</code> function is called to eventually transmit the packet out the interface toward its next hop.</p><p>Herein lies the obstacles transparent proxying must overcome.</p><ol><li><p>The routing layer must accept the packet for local delivery, not forward it.</p></li><li><p>Once accepted for local delivery, the proxy needs to deliver it to the proxy's TCP socket bound to a port that does not match the packet's destination.</p></li><li><p>Once the client side is connected the proxy must connect to the server with the original client's address and port.</p></li><li><p>The return traffic must pass through the proxy to ensure TCP connections are properly maintained.</p></li></ol><p>Let's tackle these individually.</p><h2>Obstacle 1: Tricking the routing layer into local delivery</h2><p>It's not really a trick, more of a common practice used in a not-so-common way.</p><p>Firstly, we can <strong>force</strong> a packet, based on its destination, to be locally delivered with a specific routing table entry.</p><p>This is what the <code>local</code> keyword does when adding a route with <code>ip route</code>.</p><p>Let's take this route for example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">ip route add local 0.0.0.0/0 dev lo</code></pre></div><p>This route would take all traffic and accept it as local delivery.</p><p>Of course, it's not that simple, since this will also route traffic <strong>leaving</strong> the host, back into the network stack, which is not what we want.</p><p>We will need to use policy routing to select the traffic we want to force local delivery for.</p><p>This is done with a combination of packet marking, IP rules, and a dedicated routing table.</p><p>We can configure <code>iptables</code> to mark all packets destined for the echo server <code>10.0.6.1</code> with the value <code>0x1</code>.</p><p>Next, we can create a policy routing rule with the <code>ip rule</code> tool, telling the kernel to use routing table <code>100</code> when performing route lookups for packets with the <code>0x1</code> mark.</p><p>Finally, we add the route we discussed earlier into table <code>100</code>, instructing the kernel to deliver these packets locally, even if the IP address is not present on the host.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">diff --git a/Makefile b/Makefile
index ef27ffb..1ce58e5 100644
--- a/Makefile
+++ b/Makefile
@@ -41,6 +41,12 @@ topology:
        $(NETNS_EXEC) $(NETNS_PROXY) sysctl -w net.ipv4.ip_forward=1
        $(NETNS_EXEC) $(NETNS_PROXY) sysctl -w net.ipv4.conf.all.proxy_arp=1

+       # configure policy routing for proxy namespace, marking traffic destined for echo server,
+       # and routing this traffic into the host for local delivery.
+       $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -d 10.0.6.1 -j MARK --set-mark 1
+       $(NETNS_EXEC) $(NETNS_PROXY) ip rule add fwmark 1 table 100
+       $(NETNS_EXEC) $(NETNS_PROXY) ip route add table 100 local 0.0.0.0/0 dev lo
+
 topology-destroy:
        $(NETNS) del $(NETNS_CLIENT)
        $(NETNS) del $(NETNS_PROXY)</code></pre></div><p>The above diff in the <code>Makefile</code> does exactly this.</p><p>If we now rebuild the topology, start the echo and proxy servers, and issue the client request, you will see it just hang.</p><p>This is because, despite being accepted for local delivery by the routing subsystem, layer 4 cannot find a socket for the ingress packet.</p><h2>Obstacle 2: Layer 4 socket delivery</h2><p>To deliver the packet to the proxy, we must hijack layer 4 delivery.</p><p>The current socket state in the proxy network namespace results in a dropped packet.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">                                                                     
                             &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; 
                             &#9474;          proxy ns            &#9474; 
 &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;&#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9474; 
 &#9474;10.0.5.1 -&gt; 10.0.6.10:8080&#9532;&#9532;&#9472;&#9472;&#9472;&#9472;&#9658;     layer 2       &#9474;     &#9474; 
 &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&#9474;    &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9474; 
                             &#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9660;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9474; 
                             &#9474;    &#9474;     layer 3       &#9474;     &#9474; 
                             &#9474;    &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9474; 
                             &#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9660;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9474; 
                             &#9474;    &#9474;     layer 4 (DROP)&#9474;     &#9474; 
                             &#9492;&#9472;&#9472;&#9472;&#9472;&#9492;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;&#9524;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; 
                                   &#9474;listening *:6666 &#9474;        
                                   &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; </code></pre></div><p>We can accept the packet now for local delivery and get past layer 3.</p><p>However, layer 4 will attempt to lookup a listening socket bound to either <code>10.0.6.10:8080</code> or the wild card <code>*:8080</code> which does not exist in this namespace and drop the packet.</p><p>We need a form of socket redirection which does not rely on modifying the packet's destination port, like a NAT function would.</p><p>This exists and is primarily implemented by the <code>TPROXY</code> <code>iptables</code> target, though, it requires a change in the application code as well.</p><p>For <code>TPROXY</code> redirection to work we must do two things:</p><ol><li><p>Introduce a new <code>iptables</code> rule which redirects the marked traffic to the proxy port for delivery</p></li><li><p>Ensure the proxy's listening socket is set to <code>IP_TRANSPARENT</code></p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">diff --git a/Makefile b/Makefile
index 1ce58e5..8840527 100644
--- a/Makefile
+++ b/Makefile
@@ -47,6 +47,10 @@ topology:
        $(NETNS_EXEC) $(NETNS_PROXY) ip rule add fwmark 1 table 100
        $(NETNS_EXEC) $(NETNS_PROXY) ip route add table 100 local 0.0.0.0/0 dev lo

+       # configure iptables to intercept marked packets toward the echo server and
+       # deliver to the proxy's port.
+       $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -p tcp -m mark --mark 1 -j TPROXY --on-port 6666
+
 topology-destroy:
        $(NETNS) del $(NETNS_CLIENT)
        $(NETNS) del $(NETNS_PROXY)
diff --git a/proxy.c b/proxy.c
index 1f51994..a1be617 100644
--- a/proxy.c
+++ b/proxy.c
@@ -65,6 +65,12 @@ int main(int argc, char* argv[]) {
         exit(EXIT_FAILURE);
     }

+    int one = 1;
+    if (setsockopt(sock, SOL_IP, IP_TRANSPARENT, &amp;one, sizeof(one)) &lt; 0) {
+        perror("setsockopt");
+        exit(EXIT_FAILURE);
+    }
+
     struct sockaddr_in addr = {
         .sin_addr = 0, .sin_port = htons(PROXY_PORT), .sin_family = AF_INET};
     if (bind(sock, (struct sockaddr*)&amp;addr, sizeof(addr)) &lt; 0) {</code></pre></div><p>The newly introduced <code>TPROXY</code> rule runs in the <code>mangle</code> table, prior to any routing decision being made on the packet.</p><p>It will run for any packets with the <code>0x1</code> mark and will <strong>transparently</strong> redirect the packet to the proxy's listening port.</p><p>Next, we update the proxy's code to enable <code>IP_TRANSPARENT</code> on the listening socket.</p><p>This allows the proxy to accept connections for destinations it is not bound to, exactly what we need to accept a <code>SYN</code> packet destined to the echo server's <code>10.0.6.1</code> address.</p><p>If we re-deploy the topology and send a request, guess what?</p><p>Everything will work!</p><p><code>term2</code>:  proxy</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">$ sudo ip netns exec proxy ./proxy
proxy start *:6666
new client: 10.0.5.1</code></pre></div><p><code>term3</code>: echo server</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">$ sudo ip netns exec server ncat -v -l -k -p 8080 --exec /usr/bin/cat
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::8080
Ncat: Listening on 0.0.0.0:8080
Ncat: Connection from 10.0.6.10.
Ncat: Connection from 10.0.6.10:45318.</code></pre></div><p><code>term1</code>, the client, is omitted since we simply run the same command as previously shown.</p><p>The client's packets were locally delivered and redirected into the proxy's listening socket.</p><p>Now, the kernel is smart here, and when it <code>accept</code>s the connection with the client, sending the <code>SYN,ACK</code> packet back, it uses the original destination as its source.</p><p>We can see this with <code>tcpdump</code> in the client's namespace.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">$ sudo ip netns exec client tcpdump -i any -nnn
18:45:07.222064 client-a Out IP 10.0.5.1.54904 &gt; 10.0.6.1.8080: Flags [S], seq 3819568740, win 64240, options [mss 1460,sackOK,TS val 479682827 ecr 0,nop,wscale 10], length 0
18:45:07.222247 client-a In  IP 10.0.6.1.8080 &gt; 10.0.5.1.54904: Flags [S.], seq 4110014804, ack 3819568741, win 65160, options [mss 1460,sackOK,TS val 3553422169 ecr 479682827,nop,wscale 10], length 0
18:45:07.222299 client-a Out IP 10.0.5.1.54904 &gt; 10.0.6.1.8080: Flags [.], ack 1, win 63, options [nop,nop,TS val 479682828 ecr 3553422169], length 0</code></pre></div><p>In the above dump we see the client sending a <code>SYN</code> to the echo server.</p><p>Once the proxy accepts the incoming packet, because the connection is over a transparent socket, the kernel is smart and will send the <code>SYN,ACK</code> and all subsequent traffic sourced as the destination of the <code>SYN</code>.</p><p>What we've built so far is an example of a "non-fully transparent proxy", for lack of a better term.</p><p>We are transparent for the client, but the server still sees the traffic originating from the proxy, not the original client.</p><p>Let's make the proxy fully transparent next.</p><h2>Obstacle 3 and 4: transparent server connect</h2><p>In the previous sections we have the proxy performing client side transparency, but the connection to the server is still made from the proxy's source address.</p><p>Typically, Linux will not allow an application to bind to an address that's not assigned to an interface on the host.</p><p>This can be, yet again, overcome with the <code>IP_TRANSPARENT</code> socket option coupled with the <code>bind</code> function call.</p><p>However, this is not enough.</p><p>Consider, the reply traffic entering the proxy's network namespace now.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">       &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;                                        
       &#9474;                            &#9474;                                        
       &#9474;         proxy ns           &#9474;                                        
       &#9474;                            &#9474;            echo server response        
       &#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488; 
       &#9474;    &#9474;     layer 2     &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9532; 10.0.6.10:8080 -&gt; 10.0.5.1:1234 &#9474; 
       &#9474;    &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9516;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9474;    &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496; 
       &#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9660;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9474;                                        
 &#9668;&#9472;&#9472;&#9472;&#9472;&#9472;&#9532;&#9472;&#9472;&#9472;&#9472;&#9532;     layer 3     &#9474;     &#9474;                                        
       &#9474;    &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9474;                                        
       &#9474;    &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;     &#9474;                                        
       &#9474;    &#9474;     layer 4     &#9474;     &#9474;                                        
       &#9474;    &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;     &#9474;                                        
       &#9474;                            &#9474;                                        
       &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;                                        </code></pre></div><p>The reply goes through layer 2 into layer 3 where it is simply routed out.</p><p>This is because there is no mechanism to deliver the return traffic locally and perform the subsequent socket redirection required.</p><p>The return traffic is unique however, since a socket which matches the return packets will exist.</p><p>We can exploit this fact and use a <code>socket</code> <code>iptables</code> match function.</p><p>The <code>socket</code> match function will inspect the incoming tuple and determine if a socket lookup for it would successfully return a socket, which in our reply case above, it would.</p><p>We can then pair this match with a <code>MARK</code> target to mark the packet for local delivery, using the same exact policy routing mechanisms we already configured.</p><p>Once delivered locally, layer 4 will attempt the same socket lookup the <code>socket</code> match function did, find the transparent socket, and deliver it to the transparent proxy.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">diff --git a/Makefile b/Makefile
index 8840527..89e07f0 100644
--- a/Makefile
+++ b/Makefile
@@ -50,6 +50,7 @@ topology:
        # configure iptables to intercept marked packets toward the echo server and
        # deliver to the proxy's port.
        $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -p tcp -m mark --mark 1 -j TPROXY --on-port 6666
+       $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j MARK --set-mark 1

 topology-destroy:
        $(NETNS) del $(NETNS_CLIENT)
        $(NETNS) del $(NETNS_PROXY)
diff --git a/proxy.c b/proxy.c
index a1be617..3f06a26 100644
--- a/proxy.c
+++ b/proxy.c
@@ -11,7 +11,7 @@
 #define PROXY_TARGET_IP 0x0a000601
 #define MAX_MSG_SIZE 1024 * 1024

-int proxy_do(int client_sock) {
+int proxy_do(int client_sock, struct sockaddr_in *client) {
     struct sockaddr_in server_addr = {.sin_addr = htonl(PROXY_TARGET_IP),
                                       .sin_port = htons(PROXY_TARGET_PORT),
                                       .sin_family = AF_INET};
@@ -25,6 +25,17 @@ int proxy_do(int client_sock) {
         return 0;
     }

+    int one = 1;
+    if (setsockopt(server_sock, SOL_IP, IP_TRANSPARENT, &amp;one, sizeof(one)) &lt; 0) {
+        perror("setsockopt");
+               goto cleanup;
+    }
+
+       if(bind(server_sock, (struct sockaddr *)client, sizeof(struct sockaddr_in)) &lt; 0 ) {
+               perror("bind");
+               goto cleanup;
+       }
+
     if (connect(server_sock, (struct sockaddr*)&amp;server_addr,
                 sizeof(server_addr)) &lt; 0) {
         perror("proxy_do: connect");
@@ -102,7 +113,7 @@ int main(int argc, char* argv[]) {
         }

         printf("new client: %s\n", buf);
-        proxy_do(client_sock);
+        proxy_do(client_sock, &amp;client);
         close(client_sock);
     }
 }</code></pre></div><p>Rebuild the proxy and redeploy the topology.</p><p>Issuing a new client request will now show a complete transparent proxy flow!</p><p>The interesting bit is the output of the echo server</p><p><code>term3</code>: connection received from proxy, using client's address.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Ncat: Connection from 10.0.5.1.
Ncat: Connection from 10.0.5.1:44730.</code></pre></div><p>Congratulations you have a fully transparent proxy server!</p><h2>TPROXY internals</h2><p>We now understand the full end-to-end configuration of transparent proxying in the Linux kernel.</p><p>Let's turn our attention to how the crux of the operation works, the <code>TPROXY</code> target.</p><p>The <code>TPROXY</code> target is implemented within the kernel's source code located in the file <a href="https://elixir.bootlin.com/linux/latest/source/net/netfilter/xt_TPROXY.c#L1"><code>xt_TPROXY.c</code></a>.</p><p>The function which is invoked when the target is expressed follows.</p><p><code>xt_TPROXY.c</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">static unsigned int
tproxy_tg4(struct net *net, struct sk_buff *skb, __be32 laddr, __be16 lport,
&#9;   u_int32_t mark_mask, u_int32_t mark_value)
{
&#9;const struct iphdr *iph = ip_hdr(skb);
&#9;struct udphdr _hdr, *hp;
&#9;struct sock *sk;

&#9;hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &amp;_hdr);
&#9;if (hp == NULL)
&#9;&#9;return NF_DROP;

&#9;/* check if there's an ongoing connection on the packet
&#9; * addresses, this happens if the redirect already happened
&#9; * and the current packet belongs to an already established
&#9; * connection */
&#9;sk = nf_tproxy_get_sock_v4(net, skb, iph-&gt;protocol,
&#9;&#9;&#9;&#9;   iph-&gt;saddr, iph-&gt;daddr,
&#9;&#9;&#9;&#9;   hp-&gt;source, hp-&gt;dest,
&#9;&#9;&#9;&#9;   skb-&gt;dev, NF_TPROXY_LOOKUP_ESTABLISHED);
&#9;laddr = nf_tproxy_laddr4(skb, laddr, iph-&gt;daddr);
&#9;if (!lport)
&#9;&#9;lport = hp-&gt;dest;

&#9;/* UDP has no TCP_TIME_WAIT state, so we never enter here */
&#9;if (sk &amp;&amp; sk-&gt;sk_state == TCP_TIME_WAIT)
&#9;&#9;/* reopening a TIME_WAIT connection needs special handling */
&#9;&#9;sk = nf_tproxy_handle_time_wait4(net, skb, laddr, lport, sk);
&#9;else if (!sk)
&#9;&#9;/* no, there's no established connection, check if
&#9;&#9; * there's a listener on the redirected addr/port */
&#9;&#9;sk = nf_tproxy_get_sock_v4(net, skb, iph-&gt;protocol,
&#9;&#9;&#9;&#9;&#9;   iph-&gt;saddr, laddr,
&#9;&#9;&#9;&#9;&#9;   hp-&gt;source, lport,
&#9;&#9;&#9;&#9;&#9;   skb-&gt;dev, NF_TPROXY_LOOKUP_LISTENER);

&#9;/* NOTE: assign_sock consumes our sk reference */
&#9;if (sk &amp;&amp; nf_tproxy_sk_is_transparent(sk)) {
&#9;&#9;/* This should be in a separate target, but we don't do multiple
&#9;&#9;   targets on the same rule yet */
&#9;&#9;skb-&gt;mark = (skb-&gt;mark &amp; ~mark_mask) ^ mark_value;
&#9;&#9;nf_tproxy_assign_sock(skb, sk);
&#9;&#9;return NF_ACCEPT;
&#9;}

&#9;return NF_DROP;
}

static unsigned int
tproxy_tg4_v0(struct sk_buff *skb, const struct xt_action_param *par)
{
&#9;const struct xt_tproxy_target_info *tgi = par-&gt;targinfo;

&#9;if (par-&gt;fragoff)
&#9;&#9;return NF_DROP;

&#9;return tproxy_tg4(xt_net(par), skb, tgi-&gt;laddr, tgi-&gt;lport,
&#9;&#9;&#9;  tgi-&gt;mark_mask, tgi-&gt;mark_value);
}</code></pre></div><p>The <code>tproxy_tg4_v0</code> function exists solely to extract the arguments of the <code>TPROXY</code> target and supply them to <code>tproxy_tg4</code> proper.</p><p>Consider the target used in this scenario.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">iptables -t mangle -A PREROUTING -p tcp -m mark --mark 1 -j TPROXY --on-port 6666</code></pre></div><p>We will be supplying the <code>tgi-&gt;lport</code> argument to <code>tproxy_tg4</code> given the <code>--on-port</code> target argument translation.</p><p>We won't be supplying either of the <code>tgi-&gt;mark_mask</code> or <code>tgi-&gt;laddr</code> arguments, though it is nice to know they exist:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">       --on-ip address
              This  specifies a destination address to use. By default the address is the IP address of the incoming interface. This is only valid if
              the rule also specifies -p tcp or -p udp.

       --tproxy-mark value[/mask]
              Marks packets with the given value/mask. The fwmark value set here can be used by advanced routing. (Required for transparent  proxying
              to work: otherwise these packets will get forwarded, which is probably not what you want.)</code></pre></div><p>Let's turn our attention to the <code>tproxy_tg4</code> function now.</p><p><code>1.</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">const struct iphdr *iph = ip_hdr(skb);
&#9;struct udphdr _hdr, *hp;
&#9;struct sock *sk;

&#9;hp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(_hdr), &amp;_hdr);
&#9;if (hp == NULL)
&#9;&#9;return NF_DROP;

&#9;/* check if there's an ongoing connection on the packet
&#9; * addresses, this happens if the redirect already happened
&#9; * and the current packet belongs to an already established
&#9; * connection */
&#9;sk = nf_tproxy_get_sock_v4(net, skb, iph-&gt;protocol,
&#9;&#9;&#9;&#9;   iph-&gt;saddr, iph-&gt;daddr,
&#9;&#9;&#9;&#9;   hp-&gt;source, hp-&gt;dest,
&#9;&#9;&#9;&#9;   skb-&gt;dev, NF_TPROXY_LOOKUP_ESTABLISHED);</code></pre></div><p>The above gets a pointer to the packet's header and stores it into a <code>struct udphdr</code>, which is used regardless of whether layer 4 is UDP or TCP, since both protocols' wire formats start with the source and destination ports.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct tcphdr {
&#9;__be16&#9;source;
&#9;__be16&#9;dest;
...

struct udphdr {
&#9;__be16&#9;source;
&#9;__be16&#9;dest;
...</code></pre></div><p>Next, a socket lookup is attempted for the current packet header details.</p><p>This socket lookup is done to find <code>connected</code> sockets that were <strong>already</strong> accepted by our transparent proxy and used to forward the request to the server.</p><p><code>2.</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">&#9;laddr = nf_tproxy_laddr4(skb, laddr, iph-&gt;daddr);
&#9;if (!lport)
&#9;&#9;lport = hp-&gt;dest;</code></pre></div><p>Next, the <code>laddr</code> and <code>lport</code> arguments are resolved, preferring the arguments to the <code>TPROXY</code> target or using the packet's wire data in lieu of the former.</p><p><code>3.</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">&#9;/* UDP has no TCP_TIME_WAIT state, so we never enter here */
&#9;if (sk &amp;&amp; sk-&gt;sk_state == TCP_TIME_WAIT)
&#9;&#9;/* reopening a TIME_WAIT connection needs special handling */
&#9;&#9;sk = nf_tproxy_handle_time_wait4(net, skb, laddr, lport, sk);
&#9;else if (!sk)
&#9;&#9;/* no, there's no established connection, check if
&#9;&#9; * there's a listener on the redirected addr/port */
&#9;&#9;sk = nf_tproxy_get_sock_v4(net, skb, iph-&gt;protocol,
&#9;&#9;&#9;&#9;&#9;   iph-&gt;saddr, laddr,
&#9;&#9;&#9;&#9;&#9;   hp-&gt;source, lport,
&#9;&#9;&#9;&#9;&#9;   skb-&gt;dev, NF_TPROXY_LOOKUP_LISTENER);</code></pre></div><p>Let's brush over the <code>TIME_WAIT</code> complexities for now.</p><p>If no socket was found, this is a new packet never seen by our transparent proxy before.</p><p>At this point, the kernel will perform a socket lookup using the arguments provided to the target.</p><p>This lookup will find the transparent proxy's listening socket, noting the <code>NF_TPROXY_LOOKUP_LISTENER</code> argument to <code>nf_tproxy_get_sock_v4</code>.</p><p>Because the proxy is listening on <code>*:6666</code> and the lookup is being done with <code>lport == 6666</code>, the proxy's listening socket will be found.</p><p><code>4.</code></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">&#9;/* NOTE: assign_sock consumes our sk reference */
&#9;if (sk &amp;&amp; nf_tproxy_sk_is_transparent(sk)) {
&#9;&#9;/* This should be in a separate target, but we don't do multiple
&#9;&#9;   targets on the same rule yet */
&#9;&#9;skb-&gt;mark = (skb-&gt;mark &amp; ~mark_mask) ^ mark_value;
&#9;&#9;nf_tproxy_assign_sock(skb, sk);
&#9;&#9;return NF_ACCEPT;
&#9;}</code></pre></div><p>Finally, if a socket was found previously, and the socket is marked transparent, an optional mark value is applied and, crucially, the <code>skb</code> is assigned the socket.</p><p>Once the <code>skb</code> is assigned the socket, it will now continue to layer 3, where our policy routing rules exist to force it into the local delivery path.</p><p>The <code>skb</code> then continues to layer 4, but now a socket is <strong>already</strong> assigned to the <code>skb</code>.</p><p>This socket will be used for subsequent delivery to an application, finalizing the usage of the <code>TPROXY</code> target.</p><h2>Cleaning things up</h2><p>For the sake of a clear explanation I took the longer route to achieve the full transparent proxy demo.</p><p>For one, its so common to mark packets for <code>TPROXY</code> that the <code>TPROXY</code> target can do this on its own.</p><p>We can remove the <code>MARK</code> rule and tell <code>TPROXY</code> to do the marking for us, collapsing the two rules for the client side leg to one.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">diff --git a/Makefile b/Makefile
index 89e07f0..87f25be 100644
--- a/Makefile
+++ b/Makefile
@@ -43,13 +43,12 @@ topology:

        # configure policy routing for proxy namespace, marking traffic destined for echo server,
        # and routing this traffic into the host for local delivery.
-       $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -d 10.0.6.1 -j MARK --set-mark 1
        $(NETNS_EXEC) $(NETNS_PROXY) ip rule add fwmark 1 table 100
        $(NETNS_EXEC) $(NETNS_PROXY) ip route add table 100 local 0.0.0.0/0 dev lo

        # configure iptables to intercept marked packets toward the echo server and
        # deliver to the proxy's port.
-       $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -p tcp -m mark --mark 1 -j TPROXY --on-port 6666
+       $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -p tcp -d 10.0.6.1 -j TPROXY --on-port 6666 --tproxy-mark 1
        $(NETNS_EXEC) $(NETNS_PROXY) iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j MARK --set-mark 1

 topology-destroy:</code></pre></div><p>This is optional of course, but is preferred, especially on nodes with a lot of <code>iptables</code> rules.</p><h2>Summing things up</h2><p>I always enjoy going over the transparent proxy flow.</p><p>It's a clinic on how to think about Linux networking, or networking in general, in layers.</p><p>Once we start to understand that each layer of the network stack has a set of rules, we can start to understand how to bend them to get to a solution like transparent proxying.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://allhailthetopology.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">All Hail The Topology is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Welcome from the topology]]></title><description><![CDATA[Hello all.]]></description><link>https://allhailthetopology.substack.com/p/welcome-from-the-topology</link><guid isPermaLink="false">https://allhailthetopology.substack.com/p/welcome-from-the-topology</guid><dc:creator><![CDATA[AllHailTheTopology]]></dc:creator><pubDate>Sat, 27 Jun 2026 22:34:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VPdD!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd2b99c97-588d-41c9-b59a-034c6467f8be_571x571.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello all.</p><p>I&#8217;d like to introduce everyone to the &#8220;All Hail The Topology&#8221; Substack.</p><p>If you don&#8217;t know who I am, my name is Louis DeLosSantos. I&#8217;ve been working in Linux kernel networking for a bit. I have a few kernel contributions under my belt and I&#8217;m currently a core maintainer for the Cilium project.</p><p>Back at my personal blog, https://who.ldelossa.is, I would post about various technical subjects I encountered throughout my day.</p><p>For a while now, I&#8217;ve wanted to focus my content strictly on Linux kernel networking and systems programming. One motivating factor is the massive disconnect between high-level configuration and what is actually going on in the source code. There is an entire world of details in just the Linux networking stack alone that remains largely tribal knowledge.</p><p>That&#8217;s what this Substack is all about. &#8220;All Hail The Topology&#8221; will be a place to learn about Linux networking and systems internals, hands-on, with both text and video media.</p><p>My plan is for every post&#8217;s text to be free. With every post, however, there will be accompanying video content where we work through the post&#8217;s contents, write code, and use tools like <code>bpftrace</code> and <code>gdb</code> to debug. Individuals who want to engage deeper with the content can become paid subscribers to access the video content.</p><p>For our first post, we will be digging into the details of how Linux&#8217;s transparent proxying works. We&#8217;ll write a standard C proxy server and, over several iterations, transform it into a true transparent proxy, breaking down each kernel mechanisms at the source code level as we go.</p><p>If this sounds interesting to you, go ahead and subscribe to stay tuned!. </p><p></p><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://allhailthetopology.substack.com/subscribe?utm_source=email&r=&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://allhailthetopology.substack.com/subscribe?utm_source=email&r="><span>Subscribe</span></a></p><p></p>]]></content:encoded></item></channel></rss>