Run Cloudflare WARP in Docker

Cloudflare WARP is a free VPN service provided by Cloudflare. As most service providers consider its exit IP as a reputable residential broadband IP, many people use it for accessing websites that have strict risk control policies, especially when their server’s IP address is not clean. However, when we use it on our own servers, we may encounter the following issues:

  • The official WARP client, in the default mode 1.1.1.1 with WARP, blocks all inbound connections, which means that websites and services on servers cannot be accessed.
  • Although the official WARP client in Local Proxy mode does not have the problem of blocking inbound connections, the HTTPS/SOCKS5 proxy it provides cannot transmit UDP packets.
  • In order to prevent abuse, Cloudflare blocks third-party clients (such as wgcf) from accessing WARP services in some regions, and it is currently unknown whether this measure will be extended to other regions.

This article will run the official WARP client in Docker to solve the above problem. The project is published on GitHub.

TL;DR

This article was written on July, 2023. Since then, the warp-docker project has undergone a series of updates. Please refer to the instructions in the GitHub repository for the latest usage.

Start the container

To run the WARP client in Docker, just write the following content to docker-compose.yml and run docker-compose up -d.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: '3'

services:
warp:
image: caomingjun/warp
container_name: warp
restart: always
ports:
- '1080:1080'
environment:
- WARP_SLEEP=2
# - WARP_LICENSE_KEY= # optional
cap_add:
- NET_ADMIN
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- ./data:/var/lib/cloudflare-warp

Try it out to see if it works:

1
curl --socks5 127.0.0.1:1080 https://cloudflare.com/cdn-cgi/trace

If the output contains warp=on or warp=plus, the container is working properly. If the output contains warp=off, it means that the container failed to connect to the WARP service.

Configuration

You can configure the container through the following environment variables:

  • WARP_SLEEP: The time to wait for the WARP daemon to start, in seconds. The default is 2 seconds. If the time is too short, it may cause the WARP daemon to not start before using the proxy, resulting in the proxy not working properly. If the time is too long, it may cause the container to take too long to start. If your server has poor performance, you can increase this value appropriately.

  • WARP_LICENSE_KEY: The license key of the WARP client, which is optional. If you have subscribed to WARP+ service, you can fill in the key in this environment variable. If you have not subscribed to WARP+ service, you can ignore this environment variable.

Data persistence: Use the host volume ./data to persist the data of the WARP client. You can change the location of this directory or use other types of volumes. If you modify the WARP_LICENSE_KEY, please delete the ./data directory so that the client can detect and register again.

Change proxy type

The container uses GOST to provide proxy, where the environment variable GOST_ARGS is used to pass parameters to GOST. The default is -L :1080, that is, to listen on port 1080 in the container at the same time through HTTP and SOCKS5 protocols. If you want to have UDP support or use advanced features provided by other protocols, you can modify this parameter. For more information, refer to GOST documentation.

If you modify the port number, you may also need to modify the port mapping in the docker-compose.yml.

Health check

The health check of the container will verify if the WARP client inside the container is working properly. If the check fails, the container will automatically restart. Specifically, 15 seconds after starting, a check will be performed every 15 seconds. If the inspection fails for 3 consecutive times, the container will be marked as unhealthy and trigger an automatic restart.

1
2
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS "https://cloudflare.com/cdn-cgi/trace" | grep -qE "warp=(plus|on)" || exit 1

If you don't want the container to restart automatically, you can remove restart: always from the docker-compose.yml. You can also modify the parameters of the health check through the docker-compose.yml.

How it works?

The following explains the working principle of the container, for reference to developers who want to improve this container or build similar ones. If you just want to use the container, you only need to read the above section.

This image includes two components, Cloudflare WARP and GOST. The WARP client is used to connect to the WARP service, and GOST is used to provide a proxy. Its inspiration initially came from a post on hostloc (in chinese). This post was the earliest attempt (that I could find) to run WARP in a container, and provided a lot of useful information, but had the following shortcomings:

  • Still using the third-party client WGCF, which may result in being banned by Cloudflare.
  • Requires interactive operations, which is inconvenient for deployment.

Based on this, I have made a series of modifications and uploaded the code to GitHub.

Official WARP client

The process of downloading and installing the official WARP client is well described in the WARP documentation. Here, we focus on how to run it in a containerized environment, which involves three parts:

  • Creating a TUN device, which is not available by default in a container.
  • Starting the WARP daemon. Normally, the WARP installation process writes the relevant configurations for the daemon into systemd or a similar tool, which allows it to start automatically at boot time. However, in a container environment, we need to start it manually.
  • Granting sufficient permissions.

The first two issues are addressed in entrypoint.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# create a tun device
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun

# start the daemon
warp-svc &

# sleep to wait for the daemon to start, default 2 seconds
sleep "$WARP_SLEEP"

# if /var/lib/cloudflare-warp/reg.json not exists, register the warp client
if [ ! -f /var/lib/cloudflare-warp/reg.json ]; then
warp-cli register && echo "Warp client registered!"
# if a license key is provided, register the license
if [ -n "$WARP_LICENSE_KEY" ]; then
echo "License key found, registering license..."
warp-cli set-license "$WARP_LICENSE_KEY" && echo "Warp license registered!"
fi
# connect to the warp server
warp-cli connect
else
echo "Warp client already registered, skip registration"
fi

It should be noted that:

  • The creation of TUN device cannot be placed in the Dockerfile because /dev is mounted as tmpfs, which is not in the file system /.
  • We need to wait for the daemon to start before using the warp-cli command. Currently, we use a pre-configured wait time. If you have a better method, please feel free to suggest it.

The permission issue can be resolved in the docker-compose.yml:

1
2
3
4
5
cap_add:
- NET_ADMIN
sysctls:
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv4.conf.all.src_valid_mark=1

This gives containers enough, but not excessive, privileges.

I have a strong aversion to containers that require --privileged=true, because that means the container gains complete root access to the host machine, which is extremely dangerous. Even if we trust the developers, there may still be security vulnerabilities in the services running inside the container, and once an attacker gains access to the container, they can directly obtain full control over the host machine. In most cases, containers do not need such high privileges, and this requirement is often a lazy behavior of developers who do not want to confirm the necessary permissions, but seriously endangers users' security.

GOST

GOST is a very powerful proxy software. In this container, I use it to provide proxy services and configure the proxy type through the GOST_ARGS environment variable.

Health check

Docker officially does not recommend running multiple processes in one container, partly because the crash of a subprocess may not be detected. However, in this particular container, I avoided this issue. The container runs the WARP daemon process and GOST proxy. If the GOST proxy crashes, it will cause the main process (entrypoint.sh) to exit, triggering an automatic restart. If the WARP daemon process crashes, it will cause the health check to fail, also triggering an automatic restart. This achieves detection of subprocess crashes.

Automatic Fix for Host Connectivity

In July 2024, GitHub user @ostrolucky reported that the host might be unable to connect to the container due to traffic being intercepted by Cloudflare WARP. Upon investigation, I found that when connecting through Cloudflare Zero Trust, organization's configured split tunnel may not include the subnet where the Docker network resides. This results in traffic being routed to WARP's network interface, preventing packets sent from the container from reaching the host. Specifically, Cloudflare WARP create a routing rule (you can find it by ip rule list) to move all packets not matching a specific fwmask to a route table it created, and intercepts all packets not belonging to the split tunnel and not handled by the WARP interface using nftables.

To automatically resolve this issue, I added a script in the container's healthcheck, automatically adding necessary rules to nftables and routing rule list to ensure that traffic from containers can reach the host properly. Users can enable this feature by setting the environment variable BETA_FIX_HOST_CONNECTIVITY=1.

References

  1. Hostloc forum - inspiration (in chinese)
  2. StackOverFlow - How to create tun interface inside Docker container image?
  3. Cloudflare WARP docs
  4. GOST docs
  5. Docker docs - Run multiple services in a container

Discussion

This article does not have comments enabled. Please submit an issue in the Github repository.

Author

Cao Mingjun

Posted on

2023-07-17

Updated on

2024-08-15

Licensed under