Policy routing with Clash

I have had introduced Clash in the past and gave an introductory guide about how you can deploy it on your Linux gateway as a transparent proxy. This article will do pretty much the same, but more in-depth - and since Clash has evolved extensively with TUN, TPROXY and auto-redir support, this time we're going to do it much more simpler, and discover the massive potential you could get with Clash as your gateway.

Installation

First of all, we're going to deploy Clash Premium on the gateway. Different from Clash, it's proprietary at the moment and only compiled on Dreamacro's (author of Clash) personal computers. It has TUN support, script-based rules support, rule providers, profiling (like a built-in Prometheus exporter), eBPF and auto-redir support. We'll dive in to those later.

The latest release for linux-amd64 can be found at https://release.dreamacro.workers.dev/latest/clash-linux-amd64-latest.gz.

Download it to the gateway and unarchive:

$ wget -O clash.gz https://release.dreamacro.workers.dev/latest/clash-linux-amd64-latest.gz
--2022-08-26 15:21:38--  https://release.dreamacro.workers.dev/latest/clash-linux-amd64-latest.gz
Resolving release.dreamacro.workers.dev (release.dreamacro.workers.dev)... 104.21.37.61, 172.67.205.5, 2606:4700:3032::6815:253d, ...
Connecting to release.dreamacro.workers.dev (release.dreamacro.workers.dev)|104.21.37.61|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6204563 (5.9M) [binary/octet-stream]
Saving to: 'clash.gz'

clash.gz                          100%[==========================================================>]   5.92M  4.92MB/s    in 1.2s

2022-08-26 15:21:48 (4.92 MB/s) - 'clash.gz' saved [6204563/6204563]

$ gzip -d clash.gz
$ file clash
clash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
$ chmod +x clash
$ ./clash -v
Clash 2022.08.26-1-g8720ef5 linux amd64 with go1.19 Fri 26 Aug 2022 02:52:04 PM UTC

Okay. Now we're going to give it a place to live and put the configuration files and IP database there.

mv ./clash /usr/local/bin
mkdir -p /etc/clash
clash -d /etc/clash # this creates default configuration and downloads IP database

Lastly let's set up its systemd service file:

[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash

[Install]
WantedBy=multi-user.target

... and make it start at boot:

systemctl daemon-reload
systemctl enable clash
systemctl start clash

You can refer to the official guide here: https://github.com/Dreamacro/clash/wiki/Running-Clash-as-a-service#systemd

Configuration

As a transparent proxy, here's an example config.yml to make it work.

General

# HTTP(S) and SOCKS4(A)/SOCKS5 server on the same port
mixed-port: 7890

mode: rule

routing-mark: 6666

auto-redir:
  enable: true
  auto-route: true

# This creates TUN device named utun on Linux
tun:
  enable: true
  stack: system
  auto-route: true
  auto-detect-interface: true

# This stores proxy group selection and fake-ip mappings
# in $HOME/.config/clash/.cache
profile:
  store-selected: true
  store-fake-ip: true

Look at the auto-redir and tun section. Enabling their auto-route options, Clash automatically sets up the Linux routing table so any incoming/outgoing L2 traffic will be captured and forwarded by it. We no longer need to setup iptables manually now!

DNS, fake-ip and rules

# Required for domain-based policy routing.
dns:
  enable: true
  listen: 0.0.0.0:53
  ipv6: false # when false, response to AAAA questions will be empty

  # These nameservers are used to resolve the DNS nameserver hostnames below.
  default-nameserver:
    - 1.1.1.1
    - 8.8.8.8
    - 9.9.9.9

  enhanced-mode: fake-ip
  fake-ip-range: 198.18.0.1/16

  nameserver:
    - 'https://1.1.1.1/dns-query'
    - 8.8.8.8

This DNS server setup is particularly interesting if you haven't approached to the concept of fake-ip before. We will discuss later.

In the proxies section you setup a variety of proxy servers and proxy groups. This is a brief example, check out the official documentation here for more.

# Some example outbound proxies you can have.
proxies:
  - name: "ss3"
    type: ss
    server: server
    port: 443
    cipher: chacha20-ietf-poly1305
    password: "password"
    plugin: v2ray-plugin
    plugin-opts:
      mode: websocket

  # socks5
  - name: "socks"
    type: socks5
    server: server
    port: 443
    # username: username
    # password: password
    # tls: true
    # skip-cert-verify: true
    # udp: true

# You associate a rule with a single proxy or a proxy group.
# In a proxy group, Clash offers proxy relaying, failovering, load-balancing, benchmarking or simply you can select which proxy you want the group to use as the outbound.
# Read the official documentation at https://github.com/Dreamacro/clash/wiki/Configuration for more.
proxy-groups:
  # If you have a VPN that exposes theirselves on a TUN device,
  # Create a proxy group and use only DIRECT as outbound.
  # Specify your outbound network device in `interface-name`.
  - name: Cisco AnyConnect
    type: select
    interface-name: tun0
    proxies:
      - DIRECT

  - name: Wireguard
    type: select
    interface-name: wg0
    proxies:
      - DIRECT

After setting up proxies and proxy groups, you set up your policy rules.

# Rule providers are a collection of rules.
# You can load them in a local file or load remotely from a URL.
rule-providers:
  CloudflareIP:
    type: file
    behavior: ipcidr
    path: ./RuleProviders/CloudflareIP.yaml

# Check this out for available rule types:
# https://github.com/Dreamacro/clash/wiki/Configuration#rules
rules:
  - DOMAIN-SUFFIX,google.co.uk,Proxy
  - DOMAIN,accounts.google.co.uk,DIRECT
  - DOMAIN-SUFFIX,internal.company.com,Cisco AnyConnect
  - DOMAIN-SUFFIX,doubleclick.net,REJECT
  - RULE-SET,CloudflareIP,Wireguard
  - GEOIP,US,Wireguard
  - MATCH,DIRECT

Finally let me introduce you to fake-ip.

Simply put, Clash DNS will resolve every name as a fake-ip, which is an address in a particular IPv6 reserved address pool, by default 198.18.0.1/16.

I'll give you an example. Let's say we want to curl -v http://icanhazip.com. You would get

$ curl -v http://icanhazip.com
*   Trying 198.18.3.18:80...
* Connected to icanhazip.com (198.18.3.18) port 80 (#0)
> GET / HTTP/1.1
> Host: icanhazip.com
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 26 Aug 2022 16:45:50 GMT
< (some http response headers redacted here)
<
123.123.123.123

The process is like this (simplified):

  1. curl asks the Clash DNS for the IP address of icanhazip.com

  2. Clash finds an empty spot in 198.18.0.1/16 and answers the A question with 198.18.3.18.

  3. curl opens TCP socket to 198.18.3.18 and sends the HTTP request.

  4. The packet to 198.18.3.18 gets routed to Clash.

  5. Clash looks up in the fake-ip cache and see which hostname it is associated with: icanhazip.com in this case.

  6. Clash looks up the policy rules and see:

    1. If there's an applying rule for icanhazip.com

    2. If there's any GEOIP, IP-CIDR or IP-CIDR6 rule without no-resolve property:

      1. Clash asks every nameservers set up in dns.nameserver, namely 8.8.8.8 and 1.1.1.1 (DoT) here in this example, what is the IP address of icanhazip.com

      2. Clash then checks if the resolved IP address applies to any of the above rules.

    3. If any of the rules apply, the outbound target is selected based on the rule.

    4. Otherwise the outbound target specified in the MATCH rule is selected.

  7. Clash forwards the packet to the target selected above, and will forward back if there is a reply.

You may ask: why fake-ip? Some proxy protocols connect to the remote host by their hostname. You give them the hostname that you want to connect to. This means if you're connecting to google.com with a SOCKS5 proxy, the IP address of google.com is actually resolved on the proxy server.

Imagine this scenario without fake-ip: You're in EU, you would locally resolve google.com to their PoP in EU. Your proxy server is in AS. So connecting to the EU PoP of Google with an AS proxy is obviously a bad idea. Hence, fake-ip this offers significant speed boost on connections over proxies.

Beware that in the above example, without fake-ip, Clash would still need to resolve google.com locally even we don't actually need it. So fake-ip saves one DNS lookup. Additionally, it offers protection against DNS pollution attack -- some ISP or national firewalls give you false DNS answers. This solves it by resolving remotely.

If you do read Chinese, here's an awesome in-depth article of fake-ip and recursive DNS.

Using Clash as the gateway

In your DHCP server options, set the advertised gateway and DNS server to the IP address of Clash.

RouterOS DHCP Server Options

Your devices will now have default route to the Clash host. They will resolve everything as a fake-ip. Every packet will be intercepted by Clash and forwarded conditionally.

Controlling Clash on the fly

There are two popular options available for a Clash web interface:

  1. https://clash.razord.top (https://github.com/Dreamacro/clash-dashboard)
  2. https://yacd.haishan.me (https://github.com/haishanh/yacd)

You can either use the public URL above or host them with Clash (see config option external-ui).

Clash Dashboard Screenshot

The web interfaces connect to the external-controller port of Clash locally on your browser.

Results

IP address lookup result