Set up network edge router via V2Ray

Motivation

Hello! Long time no see! I have been heavily using an iOS app called “Quantumult X” (a.k.a. QX) these days, mainly for the following reasons:

Overall I’m very happy with this app but I do find some limitations:

For me I do have some proxy servers using shadowsocks, vmess, but I also have other types up and running such as wireguard. So natively there’s no way you can add a wireguard server to this list of server_local in QX.

Besides that I have other good-to-have features that I wish I can use within a single app:

In this post I will share how I set up such a network edge router with the help of V2Ray. Specifically I’m deploying this service to my OCI Ampere machine (arm64). Check out my previous post about setting up an OCI instance if you are interested.

Before we come to the v2ray server config itself, let’s add a few VPN servers that we will use later.

Set up a Cloudfare Warp+ VPN (or any wireguard server)

One thing great about Cloudfare Warp+ is that you can convert it to a wireguard server with the help of wgcf.

The conversion steps are simple and straightforward, just follow their official README and you should be good to go. Below is my steps for my future reference

wget https://github.com/ViRb3/wgcf/releases/download/v2.2.15/wgcf_2.2.15_linux_arm64
ln -s wgcf_2.2.15_linux_arm64 wgcf
./wgcf register -n '<machine name>' --accept-tos
WGCF_LICENSE_KEY="<your license key>" ./wgcf update
./wgcf generate

After the above steps you should have a wireguard conf file named wgcf-profile.conf by default.

Use wireguard as a socks5 server

We don’t want to directly use the generated wireguard config because it will route all our VPS traffic through Cloudfare. What we want is an application-based proxy. So here I will convert it into a socks5 proxy server by using a docker container. I know this is not the most efficient approach but this is simpler and easy to follow.

First check out the content of the generated wireguard conf file in the previous step and remove all the IPv6 contents within this file e.g., your wireguard IPv6 address, and the IPv6 CIDR in the AllowedIPs. This step is necesssary because our wireguard-socks5 docker container cannot process such rules.

Clone this repo and build your own wireguard-socks5 image. Note that you may want to change the network interface here used in the container if you encounter any issues (e.g., change it to eth0). Note that after the change you should rebuild the docker image.

Below is my steps:

git clone git@github.com:mcao2/wireguard-socks5.git
podman build -t wireguard-socks5:latest-arm .
# First run an interactive container to check if there's any errors
podman run --rm -it \
  --name=wireguard-socks-proxy \
  --device=/dev/net/tun --cap-add=NET_ADMIN --privileged \
  --publish 127.0.0.1:1080:1080 \
  --volume /my/dir/to/wireguard:/etc/wireguard:z \
  wireguard-socks5:latest-arm
# If you encounter network interface name resolution error then change it in https://github.com/mcao2/wireguard-socks5/blob/master/sockd.conf and rebuild the docker
# Now start our proxy server in detach mode
podman run --rm -d \
  --name=wireguard-socks-proxy \
  --device=/dev/net/tun --cap-add=NET_ADMIN --privileged \
  --publish 127.0.0.1:1080:1080 \
  --volume /my/dir/to/wireguard:/etc/wireguard:z \
  wireguard-socks5:latest-arm

By now you should have a socks5 server up and running in 127.0.0.1:1080, verify this by using curl --proxy socks5h://127.0.0.1:1080 ipinfo.io.

Auto start container on restart

Podman provides command to generate a systemd unit file that you can enable for this purpose. Below is my steps to enable this:

sudo su
setsebool -P container_manage_cgroup on
# `--name` is the container name
podman generate systemd --files --name wireguard-socks-proxy --new
mv container-wireguard-socks-proxy.service /etc/systemd/system/container-wireguard-socks-proxy.service
systemctl enable container-wireguard-socks-proxy.service
# Stop your existing container first
podman rm -f wireguard-socks-proxy
# Start your new container via systemd
systemctl start container-wireguard-socks-proxy.service
# Check its static
systemctl status container-wireguard-socks-proxy.service

V2Ray rules

Now is the fun part! Install the latest V2Ray service following their guides.

Edit the config file /usr/local/etc/v2ray/config.json and add the following contents:

{
  "log": {
    "loglevel": "warning",
    "access": "/var/log/v2ray/access.log",
    "error": "/var/log/v2ray/error.log"
  },
  "inbounds": [
    {
      "tag": "vmess-in",
      // CHANGE ME!
      "port": <YOUR_PORT_1>,
      "listen": "0.0.0.0",
      "protocol": "vmess",
      "settings": {
        "clients": [ // An array for valid user accounts
          {
            // CHANGE ME!
            "id": "<YOUR_UUID_1>", // User ID, in the form of a UUID
            "alterId": 64, // Number of alternative IDs, which will be generated in a deterministic way
            "level": 0 // V2Ray will apply different policies based on user level
          }
        ],
        "disableInsecureEncryption": true // Forbids client for using insecure encryption methods
      }
    },
    {
      "tag": "telegram-in",
      // CHANGE ME!
      "port": <YOUR_PORT_2>,
      "listen": "0.0.0.0",
      "protocol": "mtproto",
      "settings": {
        "users": [
          {
            "level": 0,
            // CHANGE ME!
            "secret": "<YOUR_SECRET_2>" // User secret. In Telegram, user secret must be 32 characters long, and only contains characters between 0 to 9, and ato f. You may use the following command to generate MTProto secret: `openssl rand -hex 16`
          }
        ]
      }
    },
    {
      "tag": "vmess-in-cloudfare",
      // CHANGE ME!
      "port": <YOUR_PORT_3>,
      "listen": "0.0.0.0",
      "protocol": "vmess",
      "settings": {
        "clients": [
          {
            // CHANGE ME!
            "id": "<YOUR_UUID_3>",
            "alterId": 64,
            "level": 0
          }
        ],
        "disableInsecureEncryption": true
      }
    }
  ],
  "outbounds": [
    {
      "tag": "default-out",
      "protocol": "freedom",
      "settings": {}
    },
    {
      "tag": "telegram-out",
      "protocol": "mtproto",
      "settings": {}
    },
    {
      "tag": "tailscale-out",
      "protocol": "freedom",
      "settings": {}
    },
    {
      "tag": "cloudfare-out",
      "protocol": "socks",
      "settings": {
        "servers": [
          {
            "address": "127.0.0.1",
            "port": 1080
          }
        ]
      }
    }
  ],
  "routing": { // Configuration for internal Routing strategy
    "domainStrategy": "AsIs", // domain resolution strategy
    "rules": [ // for each inbound connection, v2ray tries these rules from top down one by one. If a rule takes effect, the connection will be routed to the `outboundTag` or `balanceTag` of the rule
      { // Route traffic for the `mtproto` protocol
        "type": "field",
        "inboundTag": [
          "telegram-in"
        ],
        "outboundTag": "telegram-out"
      },
      { // Route traffic for tailscale
        "type": "field",
        "ip": [
          "100.64.0.0/10" // tailscale subnet
        ],
        "outboundTag": "tailscale-out"
      },
      { // Route vmess-in via default out
        "type": "field",
        "inboundTag": [
          "vmess-in"
        ],
        "outboundTag": "default-out"
      },
      { // Route vmess-in-cloudfare via cloudfare wireguard interface
        "type": "field",
        "inboundTag": [
          "vmess-in-cloudfare"
        ],
        "outboundTag": "cloudfare-out"
      }
    ]
  }
}

The config file is self-explanatory and you definitely need to change all the fields marked with // CHANGE ME!.

In this config file I also added a mtproto server for my telegram client to use. You can choose not to add this and safely remove all associated routing rules.

That’s it! In your QX config add the following:

vmess=<YOUR_VPS_IP>:<YOUR_PORT_1>, method=chacha20-poly1305, password=<YOUR_UUID_1>, fast-open=false, udp-relay=false, tag=vmess-ampere
vmess=<YOUR_VPS_IP>:<YOUR_PORT_3>, method=chacha20-poly1305, password=<YOUR_UUID_3>, fast-open=false, udp-relay=false, tag=vmess-ampere-cloudfare

Enjoy!