Open-source project
cloudflare/mmproxy avatar
cloudflare/mmproxy

mmproxy fakes the client address so legacy TCP servers can keep it

mmproxy, the magical PROXY protocol gateway

517 stars60 forksCBSD-3-Clause

At a glance

What is it?
mmproxy is a small C program that sits between a PROXY protocol load balancer and an application that does not support PROXY protocol, and makes the connections arrive with the original client address restored. It works by binding sockets with IP_TRANSPARENT and installing a routing table, and it insists on running on the same host as the application over loopback.
Who is it for?
mmproxy fits an operator who has put a PROXY protocol load balancer in front of a mature TCP service that cannot be changed, and it does not fit a service you can rebuild, since native PROXY protocol support removes the extra hop and the spoofing entirely. Before you run it, read the routing rules the program prints for your setup and apply them yourself, and remember that the last link is source IP spoofed and must stay on loopback on the same host as the application.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The address your server sees is the load balancer

The problem mmproxy exists to solve is stated in the README with the clarity of a bug report. Introducing TCP level load balancing creates a significant problem: the client source IP gets lost, and from the application's point of view the inbound TCP connection is originated by the load balancer rather than by the real client.

That matters for anything that logs, rate limits, geolocates or makes an authorisation decision based on the peer address. A web server behind a load balancer sees the balancer, so the client address has to be recovered by some other means.

HAProxy's PROXY protocol is the standard answer. It defines an exchange in which the first bytes transmitted from the load balancer describe the client source IP. The catch, spelled out in the README, is that applications must explicitly support proxy-protocol to use it.

For a new service, that is a line in the configuration. For many mature applications it is not, and the README says so directly: introducing proxy-protocol support is hard for them, and mmproxy is a workaround for exactly that case. The tool's own tagline, the magical PROXY protocol gateway, is a bit tongue in cheek, but the mechanism underneath is real and documented.

Where the process has to sit in the path

The nomenclature section of the README draws three diagrams, and they are the fastest way to understand the design.

The normal case is a direct connection.

code
    Client --> Application

With a PROXY protocol load balancer in front of an application that supports it, the chain gains a hop.

code
    Client --> PP-enabled Load Balancer --> PP-enabled Application

And mmproxy inserts itself between the balancer and an application that does not support it.

code
    Client --> PP-enabled Load Balancer --> mmproxy --> Application

The text around the third diagram carries the important warning. The traffic on the last link, from mmproxy to the application, is source IP spoofed and MUST be delivered over loopback. In effect, mmproxy MUST run on the same machine as the application.

The requirements list is explicit about the rest: it must be run on the very server which runs the application, and the communication between mmproxy and the application must happen over the loopback interface. It needs root permissions, or alternatively the CAP_NET_ADMIN capability. It needs a Linux kernel of at least 2.6.28. And it needs a relatively unsophisticated iptables and routing setup.

That last requirement is doing more work than it looks. The setup is not optional decoration, it is the second half of the trick, and the program will print the rules it needs so that you apply them yourself rather than having it rewrite your firewall.

IP_TRANSPARENT plus a routing table

How mmproxy performs the source IP spoofing is described in a section of its own, and it comes down to two mechanisms.

The first is the Linux IP_TRANSPARENT socket option, which the README calls an obscure option originally designed to help build transparent proxies. With it you can create sockets that send from arbitrary IP addresses, and mmproxy reuses that feature to send the application a packet that claims to come from the original client.

The second is routing. Once you are sending from a spoofed source address, Linux still has to decide where to send the reply, and by default it would take the default route out to the internet, where the packet would go nowhere. To prevent that, mmproxy deploys a custom routing table that forces the return traffic to be routed to loopback, so the reply comes back to the socket that is holding the connection open.

That is the whole design, and it explains both the power and the constraint. The application receives a connection that looks exactly like it came from the remote client, so it needs no changes at all. The price is that the kernel's routing behaviour has to be bent into shape on the host, and a wrong rule does not fail loudly, it fails by dropping return packets.

The README also notes the connection requirement in the usage help: inbound connections MUST have a proxy-protocol version 1 header and MUST be originated from the set of given source IPs. So mmproxy is not a transparent middlebox that works with anything. It only speaks the version 1 dialect of the protocol, and it validates the load balancer addresses, which is the security boundary in front of a mechanism that otherwise lets it claim any source address.

The routing rules the program prints for you

Rather than silently reconfiguring the host, mmproxy prints the rules it needs and lets you decide. There are two variants, depending on where the traffic is forwarded.

If the traffic is forwarded to the loopback interface, the rules are short: a policy routing rule that sends packets arriving on lo to table 100, and a local route in that table that treats every address as bound to loopback.

code
       ip -4 rule add from 127.0.0.1/8 iif lo table 100
       ip -6 rule add from ::1/128 iif lo table 100
       ip route add local 0.0.0.0/0 dev lo table 100
       ip -6 route add local ::/0 dev lo table 100

If the traffic is forwarded to any other interface, the setup is longer, because the packets have to be marked and remembered across the connection. The README walks through it in six numbered steps: check that a default route exists, enable route_localnet on the default interface by writing to /proc/sys, save the conntrack CONNMARK on packets carrying mark 123 in PREROUTING and restore it in OUTPUT for both iptables and ip6tables, add a rule that sends marked packets to table 100, and install the local routes there.

code
       iptables -t mangle -I PREROUTING -m mark --mark 123 -m comment --comment mmproxy -j CONNMARK --save-mark
       ip6tables -t mangle -I PREROUTING -m mark --mark 123 -m comment --comment mmproxy -j CONNMARK --save-mark
       iptables -t mangle -I OUTPUT -m connmark --mark 123 -m comment --comment mmproxy -j CONNMARK --restore-mark
       ip6tables -t mangle -I OUTPUT -m connmark --mark 123 -m comment --comment mmproxy -j CONNMARK --restore-mark
       ip rule add fwmark 123 lookup 100
       ip -6 rule add fwmark 123 lookup 100

The example run in the README shows the tool checking the rules it printed against the live system and marking each one VERIFIED as it goes, which is a much better safety net than applying a script blindly.

Allowed subnets, listen address and two targets

The command line is short. The help text gives the full form.

code
    mmproxy [ options ] --allowed-subnets FILE -l LISTEN_ADDR -4 TARGET_V4_ADDR -6 TARGET_V6_ADDR

The pieces are the file of allowed source subnets, the address to bind to, and separate IPv4 and IPv6 targets to forward to. The bind address defaults to [::]:8080. The allowed subnets are the security boundary, since only connections from those addresses are accepted, and the README notes that the inbound connections must have a proxy-protocol version 1 header and must originate from the given source IPs.

The remaining options are for the operator rather than the traffic. There is a mark option to set a specific MARK on outbound packets, which the help says is needed to play with iptables, a table option to use a specific routing table number in the printed suggestion, a quiet flag to suppress the iptables, routing and system tuning suggestions, and a verbose flag for detailed logs on stdout.

The example in the README runs it on port 2222, unpacks the proxy-protocol header and forwards to 127.0.0.1:22 on TCP.

code
    echo -en "0.0.0.0/0\n::/0\n" > allowed-networks.txt
    mmproxy --allowed-networks allowed-networks.txt -l 0.0.0.0:2222 -4 127.0.0.1:22 -6 [::1]:22

There is a small inconsistency worth knowing about before you type it. The help text documents the option as --allowed-subnets, while the example uses --allowed-networks, and the run example uses the short form -a. Any of them may work, but the documentation is not consistent, so check the actual binary with --help on your own build.

The run example shows the realistic use, which is a load balancer in front of a service and the balancer addresses fetched from the provider.

code
$ curl -s https://www.cloudflare.com/ips-v4 https://www.cloudflare.com/ips-v6 > networks.txt
$ sudo mmproxy -a networks.txt -l 0.0.0.0:2222 -4 127.0.0.1:22 -6 '[::1]:22'

A small C program with vendored dependencies

The repository is compact. The source is three files, src/main.c, src/utils.c and src/net.c, and the build is a single Makefile that links two vendored libraries.

Those libraries are libmill and libseccomp, both present as directories in the tree alongside a .gitmodules file, so they arrive as submodules rather than system packages. libmill provides the concurrency primitives, and libseccomp is the library for restricting the system calls a process is allowed to make, which fits a program that opens raw sockets and manipulates routing.

The Makefile shows the hardening the author wanted. The compiler defaults to clang, the link step uses -Wl,-z,now and -Wl,-z,relro, _FORTIFY_SOURCE=2 is set, and depending on the compiler the build adds either -fstack-protector-strong or -fstack-protector. Position independent executables are enabled with -fPIE, and the binary is linked statically against the two vendored libraries. There is a release build that adds -march=corei7, and a debugaddress build that switches on the address and undefined behaviour sanitizers, which is a sensible pair of modes for network code.

Two small conveniences in the Makefile are worth a mention. There is a format target that runs clang-format over the sources and then greps the source for TODO markers, which is a cheap way to keep a known list of unfinished items visible. And there is a rule that regenerates cloudflare-ip-ranges.txt by fetching the provider's published IPv4 and IPv6 ranges and sorting them, which is exactly the file you would feed to the allowed subnets option.

Building from source is a clone, a submodule update and make.

bash
    git clone https://github.com/cloudflare/mmproxy.git
    cd mmproxy
    git submodule update --init
    make

When the workaround is the wrong shape

The limitation of mmproxy is structural rather than a bug. It must run on the same host as the application, the last hop must be loopback, and it needs root or CAP_NET_ADMIN plus a modified routing table on a production machine. A container platform, a network namespace or a machine you do not own are all environments where this is awkward at best.

There is also a performance and complexity cost that the diagrams hide. Every connection is copied twice, once from the load balancer into mmproxy and once from mmproxy into the application, and the application has no idea that is happening. The source address it sees is a lie, and the lie is only as good as the routing rules you applied.

The real alternative is the one already drawn in the README, the PP-enabled application. If the service can speak PROXY protocol natively, that diagram has one fewer process, no spoofed traffic and no custom routing table. The difference in approach is that mmproxy adapts an application you cannot change, and native support fixes the problem at the source. mmproxy is the right answer when the application is a third party binary you will never rebuild, and the wrong answer when you own the code, because then the durable fix costs a flag in a configuration file rather than a routing table and a capability on a production host.

One more boundary is worth stating plainly. The tool accepts only version 1 headers and only connections from the allowed subnets, so a deployment that expects PROXY protocol v2, with its TLVs and its Unix socket transport, is out of scope, and the allowed subnets file is the only thing standing between the spoofing mechanism and anyone who can reach the listener.

On upkeep, the repository is not archived, the last push was on 2026-04-24, and there are no GitHub releases, so the binary is built from source rather than downloaded. The licence is BSD 3-Clause, with the file named LICENSE-BSD-CloudFlare in the repository root.

Editorial conclusion

mmproxy fits an operator who has put a PROXY protocol load balancer in front of a mature TCP service that cannot be changed, and it does not fit a service you can rebuild, since native PROXY protocol support removes the extra hop and the spoofing entirely. Before you run it, read the routing rules the program prints for your setup and apply them yourself, and remember that the last link is source IP spoofed and must stay on loopback on the same host as the application. The last push was on 2026-04-24, the project publishes no GitHub releases, and the code is under a BSD 3-Clause licence with a Cloudflare specific licence file.

Frequently asked questions

What problem does mmproxy solve?

It restores the original client IP for a TCP application that does not support the PROXY protocol. mmproxy sits between a PROXY protocol enabled load balancer and the application, strips the header and spoofs the source address so the application sees a connection from the real client.

Where does mmproxy have to run?

On the very server which runs the application, with the communication between mmproxy and the application over the loopback interface. The README is explicit that the traffic on the last link is source IP spoofed and MUST be delivered over loopback, and that mmproxy MUST run on the same machine as the application.

How do I build mmproxy?

Clone the repository, run git submodule update --init to fetch the vendored libmill and libseccomp libraries, and run make. The build defaults to clang, links the binary statically against the two libraries, and enables the address and undefined behaviour sanitizers in the debugaddress build mode.

What privileges does mmproxy need?

Root permissions, or alternatively the CAP_NET_ADMIN capability. The README also lists a Linux kernel of at least 2.6.28 and a relatively unsophisticated iptables and routing setup, and the program prints the routing and iptables rules it needs so you can apply them yourself.

Is mmproxy still being maintained?

The repository is not archived and the last push was on 2026-04-24, but the project publishes no GitHub releases, so there is no tagged binary to install and the Makefile is the supported build path. The licence is BSD 3-Clause, with the licence file named LICENSE-BSD-CloudFlare.

Official sources

  1. cloudflare/mmproxy on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cloudflare-mmproxy.svg)](https://hysenlabs.com/projects/cloudflare-mmproxy)

Was this useful?