Note: At the time this post was written, OpenWRT used iptables as the default firewall, but as of version 22.03, it was replaced with nftables, so the iptables commands discussed below will not be directly applicable.

What?

For reasons, I recently wanted to examine https traffic generated by a windows machine on my network. The excellent mitmproxy provides a simple, user-friendly way to do this, but my specific case of “use a router to selectively send traffic to a transparent proxy on a different host” wasn’t obvious in the first few pages of google and stackoverflow. So, here it is.

Holy wall of text man, just give me commands to copy / paste

OK.

The scenario

  1. Goal: Figure what some chatty crapware on a windows box (the target) is yelling at The Cloud.
  2. Configuration: A mix of Windows, Linux and mobile devices connect to the internet through an OpenWrt router in typical DHCP + NAT mode.
  3. Requirements: I only want to proxy the traffic related to the crapware, without disrupting regular traffic. The target is used for other things, and I want to capture traffic for significant time.
  4. More requirements: I’m lazy. I don’t really want to mess with moving cables around or wholesale network reconfiguration on the systems involved. I’d like to be able switch it on and off in the future with minimal fuss.

First approach - Just use proxy settings

mitmproxy offers several modes, the simplest being a regular proxy, where you configure the target system to use mitmproxy with normal OS or browser proxy settings. Doesn’t really cover #3 and #4 but, easy, right?

Unfortunately, the crapware appeared to ignore windows proxy settings. Denied. Maybe? I later found traces that at least parts of it did try to use the proxy at this point, but I didn’t know at the time. Anyway, I did want #3 and #4.

Second approach - Transparent proxy

Fortunately, mitmproxy offers transparent proxy mode for situations where the target can’t be explicitly configured to use a proxy. In this mode, traffic must be routed to the proxy at the network level. The simple / typical way to do this is to put the mitmproxy box upstream of the target, configured as a gateway.

Problem: The OpenWrt router isn’t easily capable of running mitmproxy. Its flash memory is smaller than the download, for starters. I could use a different host, but did I mention not wanting to mess with cables?

But it is a router, so let’s just route the traffic of interest to the proxy on another host. This is where googling “OpenWrt transparent proxy” “OpenWrt route to proxy” and similar was unhelpful. Results kept suggesting using iptables forward but that re-writes destination, specifically called out as the wrong way in the mitmproxy documentation. (Huzzah for not-wrong documentation.) We want to route, but only packets related to the crapware on ports 80 and 443.

Policy based routing to the rescue

For anything more complicated than “traffic for network X goes though gateway Y,” Linux offers the swiss army knife known as policy based routing. (This documentation appears largely unchanged since I first encountered it as bleeding edge, better compile your own kernel stuff in the early 2000s, but aside from the references to dialup links and some minor command differences, it seems to still be mostly valid.)

We can use iptables mark to identify the traffic, and route accordingly.

The plan

  1. An iptables rule on the OpenWrt router marks traffic, using an ipset, because the crapware uses bunch of domains with multiple IPs for load balancing and CDNs.
  2. A route rule directs this traffic to a Linux (Ubuntu 20.04, in this case) host running mitmproxy.
  3. An iptables rule on the proxy host forwards ports 80 and 443 to the proxy port 8080. mitmproxy only listens on one port, and is smart enough to sort out http and https, but the port mangling has to happen on the host running mitmproxy so it knows where to send them.
  4. tcpdump on the proxy host captures the raw packets, for later analysis in wireshark.

In the commands below, router.lan, proxybox.lan and target.lan refer to the OpenWrt router, the host running mitmproxy and the host generating traffic to be intercepted, respectively. In practice I used IP addresses. The interface br-lan refers to the internal LAN interface on router.lan, while eth0 on proxybox.lan refers to the default LAN interface. YMMV.

Setting up the proxy certificate

Normally, mitmproxy uses a special domain “mitm.it”, which when visited through the proxy provides instructions and the root certificate needed make the target accept the connection as legitimate. In the selective setup below, mitm.it would need to be included in the list of IPs to proxy, but in the interest of setting things up in advance, I just ran mitmproxy in standard mode and grabbed the cert file. It’s also worth grabbing the instructions if you need them, because they don’t seem to appear in the same form in the documentation.

I found only the windows GUI instructions worked, For me, the command line “certutil.exe -importpfx …” failed complaining the cert was self signed.

The certs are also stored in ~/.mitmproxy on the proxy host after it has been run once, and don’t change between sessions.

Setting up the route

On the router:

# install ipset support, if not already present
opkg update && opkg install ipset
# give the route table a friendly name, with arbitrary ID 101
echo 101 mitmprox >> /etc/iproute2/rt_tables

The above persist until you remove them.

# create the route rule, for arbitrary firewall mark value 101
ip rule add fwmark 101 table mitmprox
# route matching traffic to proxy host
ip route add default via proxybox.lan dev br-lan table mitmprox

# create the ipset
ipset create mitmprox hash:ip,port counters
# make a chain for the mark rule, to allow it to be configured
# before inserting it the chain that will actually affect traffic
iptables -t mangle -N mitmprox

# create the iptables mark rule
# Note the dst,dst syntax specifies how the IP and port in the ipset
# are interpreted. You need one for each field in the set
iptables -A mitmprox -i br-lan -t mangle -p tcp -s target.lan -m set --match-set mitmprox dst,dst -j MARK --set-mark 101

# add IP / port combos
ipset add mitmprox x.y.z.w,80
ipset add mitmprox x.y.z.w,443
# ... add the rest. I used a script to generate the commands from a list of IPs

Since the mitmprox chain is not yet inserted anywhere, nothing will be marked and none of the above should affect existing traffic.

In a default OpenWrt configuration, the above do not persist through reboots.

I’ve chosen to name the route table, ipset and iptables chain all “mitmprox” to indicate what they are for, but the names are all arbitrary and unrelated. The same applies to the choice of 101 for the route table ID and mark value.

“counters” in the ipset command is optional, allowing the use of “ipset list” to see how many hits there were on each ip:port combo.

Setting up the proxy

On the proxy host:

# per mitmproxy docs, ensure redirects are off
sysctl -w net.ipv4.conf.all.send_redirects=0
# enable forwarding
# note you can set the FORWARD chain policy to DROP to be sure you don't
# forward any unrelated traffic somewhere, if the system isn't supposed to be forwarding
sysctl -w net.ipv4.ip_forward=1
# forward ipv6 too, if you are using it
sysctl -w net.ipv6.conf.all.forwarding=1
# redirect to the proxy
# this allows the proxy to know the original port
iptables -t nat -I PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -I PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8080

The above assumes that there is no other web traffic being directed at the proxy host that you don’t want to proxy. If there were, you could restrict the rules by source.

Other notes

The above only includes IPv4. If you want to capture IPv6, it’s mostly the same stuff just repeated with ip6tables instead of iptables and ip -6 … instead of ip. You will need a separate ipset for IPv6 addresses, created with “family inet6”, but the route table and mark values can be the same. You may also want to disable generation of random temporary IP addresses on the target.

You probably want to examine existing iptables rules and routing rules before you start adding stuff. Be sure specify the appropriate table for iptables (-t mangle on the router, -t nat on the proxy host)

Setting it all in motion

Since parts of the crapware in question run as windows services, I shut down the target system before activating the proxy. Additionally, since a normal menu shutdown in Windows 10 hibernates without stopping / starting system services, I forced a real shutdown with

shutdown /s

On proxy host, each command in it’s own terminal:

Start mitmproxy, recording ssl key log (see here) to allow decoding in wireshark, saving captured flow to a file for later analysis.

SSLKEYLOGFILE="sslkeylogfile.txt" mitmproxy --mode transparent --showhost --save-stream-file crapware1.mitm

Start a tcpdump of proxied traffic

tcpdump -i eth0 -w crapware1.pcap host target.lan and \( tcp port 80 or tcp port 443 \)

On the router:

# enabled the rule by adding it to the mangle PREROUTING chain
iptables -t mangle -I PREROUTING -j mitmprox

The above starts marking and routing traffic.

Boot the target, and watch the requests fly.

Cleaning up

Shut down the target. Optional, but switching from the proxy mid stream will likely Break Things, and I didn’t want the crapware spewing a bunch of extraneous errors to its logs.

Quit mitmproxy with q

Kill the tcpdump with control+c

Remove the chain from pre-routing on the router

iptables -t mangle -D PREROUTING 1

At this point traffic will no longer be routed to the proxy, so you can stop here if you expect to use it again soon.

Remove the remaining iptables and routing rules from the router and proxy host.

Remove the mitmproxy cert from the target. The cert must be installed as trusted, meaning anyone with access to your mitmproxy files would be able to impersonate or mitm most sites, so you probably don’t want to leave it installed.

Remove the mitmprox line from /etc/iproute2/rt_tables on the router.

What does the crapware say?

It’s mostly boring and stupid (casablanca-shocked.gif), but that’s a story for another day.