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:
- Easy to setup and lots of tutorials/scripts online that you can add and learn
- You can add VPN servers, add flexible routing rules e.g., SSID-based switching, and subscribe to public servers (not recommended out of privacy/security concerns)
- You can do MITM rewrites that enable you to have premium features on other apps for free
Overall I’m very happy with this app but I do find some limitations:
- The set of protocols supported by QX is limited. According to their sample.conf, it only supports
shadowsocks
,vmess
,http
, andtrojan
.
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:
- iOS only supports 1 VPN up and running and therefore if I connect to QX then my tailscale connection is down. This is okay but it would be great if I can access my services deployed within the tailscale subnet.
- Even if this single app QX is capable to handle all the protocols, it is still limited to my mobile devices and I wish I can have some sort of routing done on the server side rather than on this app, so that if I switch to another app or a different platform then I can still have the same set of routing rules and servers. This is what I call the “network edge router” which is simply a VPS server that helps me route my traffic to different VPN servers.
- The added benefit of this approach is that the VPS server is much more performant and flexible since it is a Linux machine so it can do much complicated things like multi-hop connections (e.g., device A -> Network Edge Router -> VPN@Server1 -> VPN@Server2), which will greatly improve your anonymity.
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 [email protected]: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!