In fall 2024, I traveled to Japan and China to see my brother and his wife for their wedding. They met in Shanghai, where everyone seems to be impressed by his fluency in spoken and written Chinese, and he was impressed by the tree-climbing skills of a businesswoman he met 💑

My brother and his wife posing by the river in  Shanghai while wearing their fuzzy little hats

I've long been curious about how the internet censorship in China works, what it feels like to interact with it, and was excited about the opportunity to experience it firsthand.

My brother has been working through it for a long time, almost 10 years now I think. He started off using the StreisandEffect/streisand project as his main solution. It included some shell scripts and Ansible playbooks to install various VPN servers on an AWS EC2 instance.

For client software back then, he said he used

shadowrocket on iOS and shadowsocksX-NG on macOS

I did a lot of research on how people get around the censorship, the history of anti-censorship technology in china, what matters most when using various VPN/proxy solutions, etc, and came up with a plan.

  1. I would use V2Ray
    • Despite what people call it, v2ray is not really a VPN. Technically it's a hightly sophisticated "forward tunnel" system
    • This allows it to "blend into the background" more easily, and mask its presence with clever sleight of hand.
      • sleight of packets? sleight of protocols?
    • As far as I can tell, it's the most mature censorship-resistance solution avaliable
    • Client software is avaliable for iOS, Android, Mac, Windows, and Linux
  2. I would configure the clients and servers specifically to leverage the sneaky capabilities that v2ray's various softwares offer.
    • Plausible deniability that "This is just normal internet traffic 😉"
    • ℹī¸ NOTE: Configuring the client is of utmost importance, that's a large part of why v2ray can work as well as it does.
    • More information on that later.
  3. I would make several VPN servers ahead of time.
    • Idea was that in case the first one got blocked, I would have a fighting chance to figure out what went wrong and try again with the next one.

Flawless Victory

I won't bury the lede. I had zero problems browsing the web and accessing my cloud accounts while I was in China. Gmail and google maps? No problem. Tank Man? No problem. Airplane tickets, Japanese hotel bookings, bank info, the list goes on.

I did witness some of my co-travelers getting blocked at various points, but even after that happened, onboarding them to my system solved these issues and did not precipitate any issues for them or for my services. I believe it was even used to stream Netflix at one point đŸ¤Ŗ

I did have some trouble installing some Chinese apps, but I'm not sure if that was caused by the v2ray or not. Alipay worked fine, but Dianping (Similar to Yelp) didn't seem to like my phone.

Its true that this was a short duration trip ( about a week ), and I have a few unique advantages that made this easier for me to pull off, but I do believe it's not a complete fluke, and it's worth writing about.

Specifically, there were some unclear and confusing aspects of v2ray's documentation and configuration. I felt like it would be worthwhile to publish my solutions, I think they are cleaner and easier to understand than others I've seen around the web.

Server Configuration w/ Caddy

This is where I think I have novel stuff to contribute, on top of other guides that already exist.

Diagram shows client devices trying to connect thru the Chinese ISP and GFW.  Some connections are HTTPS, and some of them are using visibly different protocols. All of the visibly different protocols are blocked.  The HTTPS connections to Gmail.com are blocked. However, HTTPS directly to Chinese web services is ok, and HTTPS to some website that the GFW has never seen before is ok.  However, it's not just any normal website: Hiding behind the Caddy Server there is a v2ray tunnel server which is tunneling the HTTPS requests to Gmail

I found the v2ray server configuration slightly confusing, and had some issues getting it to work at first, especially the HTTP path. So I decided to just leave it as default as possible and handle most of the HTTP stuff in Caddy, which I'm more familiar with.

First off, the

V2Ray configuration using Docker:

docker-compose.yml

services:
  v2ray:
    image: teddysun/v2ray:5.19.0
    ports:
      - 1310:1310
    container_name: v2ray
    environment:
      - TZ=Asia/Shanghai
    restart: always
    command: /usr/bin/v2ray run -config /v2ray/config.json
    volumes:
      - ./v2ray:/v2ray

v2ray/config.json

{
  "log": {
    "access": "/var/log/v2ray/access.log",
    "loglevel": "info"
  },
  "inbounds": [
    {
      "listen": "0.0.0.0",
      "port": 1310,
      "protocol": "vmess",
      "settings": {
        "clients": [
          {
            "id": "82d984d3-1cda-4bc5-bd49-f167e73c2719",
            "alterId": 0,
            "security": "auto"
          }
        ]
      },
      "streamSettings": {
        "network": "ws",
        "wsSettings": {
          "path": "/"
        }
      },
      "mux": {
        "enabled": true
      }
    }
  ],
  "outbounds": [
    {
      "protocol": "freedom",
      "tag": "freedom"
    }
  ],
  "dns": {
    "servers": [
      "8.8.8.8",
      "8.8.4.4"
    ]
  }
}

The inbounds[0].settings.clients[0].id value acts as the v2ray server password, which has to be entered into the client for it to be able to connect.

It must be a GUID, which can be generated with the uuidgen command:

forest@debian:~$ sudo apt-get install uuid-runtime
...
forest@debian:~$ uuidgen
82d984d3-1cda-4bc5-bd49-f167e73c2719
forest@debian:~$ 

The inbounds[0].streamSettings.network = "ws" value means WebSocket. After an HTTP connection is made, it will upgrade to a WebSocket, and start the VMESS protocol from there.

This may not be the ideal configuration, but it worked for me, and seemed to be the most immediately avaliable and reliable.

Next, we have the

Caddy configuration (Not using Docker):

I could have ran Caddy in Docker as well, but the server I was installing it on already had Caddy set up running on the host, with its config file at:

/etc/caddy/Caddyfile

mail.totally-legit-domain.com {
  root * /usr/share/caddy

  route /ws_io34857 {
    rewrite /ws_io34857* /
    reverse_proxy http://localhost:1310

  }

  route /login {
    respond 401 {
      body "401 Unauthorized"
      close
    }
  }

  route {
    file_server
  }
}

What all is going on in this Caddyfile?

Lets go thru it...

  1. A separate subdomain is used for v2ray.
    • This encapsulates the v2ray config and allows it to coexist with other things on the same server.
  2. The domain name looks like it might be a mailserver. I also used other common, legit sounding subdomains like social.xyz.com and webmail.xyz.com on other servers.
    • Domain names are 100% public. The domain name is transmitted in plaintext multiple times during each web request, once by the DNS protocol and once in the SNI (server name indication) field of the TLS ClientHello packet.
  3. There are three handlers:
    • Requests to /ws_io34857 will have the /ws_io34857 path stripped and be reverse-proxied to port 1310, where our v2ray server is listening.
      • A non-guessable path is used to make it harder for a prober to analyse what this server is serving.
      • The path part of an HTTPS request is encrypted with TLS, so it won't leak like the domain name will.
    • Requests to /login will always return 401
    • All other requests will serve static files from /usr/share/caddy

Inside /usr/share/caddy, I put a slightly modified version of the login page from the alps webmail client as index.html:

A screenshot of mail.totally-legit-domain.com showing a "Webmail Login" form with Username and Password inputs.

So from the outside, this looks like a normal login page which matches the domain name. If you try to login, no matter what username and password you chose, you will always get a basic 401 Unauthorized response, which would look typical from any sort of basic web application written in Go, or really any web application that returns plain text responses for errors.

Since the v2ray requests to this domain are using TLS, the content of the requests is encrypted. And if I understand correctly, the VMESS protocol that v2ray uses specifically crafts the sizes and timings of the encrypted bytes flowing inside requests and responses to make the connection look like normal web traffic. (To some extent, it probably helps that real web traffic is being tunneled inside it!)

Client Configuration

I used v2rayNG as my primary client on Android (LineageOS), and QV2ray on Linux desktop.

I downloaded the v2rayNG APK directly from the main project's GitHub releases page:

A screenshot of the Github  releases page for v2rayNG. The release notes are in chinese. The machine translation says "Added option to bypass LAN with VPN", "Fix some Known Issues",  "Because the latest GEO file hsa been upgraded, the startup speed may not be as fast as before"

The v2rayNG configuration that worked looked like this:

A Screenshot of the v2RayNG server connection information configuration page. All of the hostnames and SNI are set to mail.totally-legit-domain.com. The "id" field is set to that GUID we generated earlier. TLS is set to TLS, transport network is set to WS, and the "ws path"  is set to /io_34857

These client configurations can be shared between apps via text or QR code, however, not all apps / versions are compatible. Worst case, you can always enter it in manually.

This is not the most important part of the configuration though, this just describes how to contact the v2ray server WHEN the client has determined that it needs to.

But the magical part that allows this project to work is all about that WHEN. The client needs a ton of smart rules and lists to use to determine when it should try to connect via the tunnel vs when it should try to connect directly.

If I remember correctly, v2rayNG comes with all of this setup already, which was a major reason why I chose to use it. I had to patch together and set it up myself on the desktop app, which took me some time, especially since most of the documentation and community is in Chinese language.

Screenshot of the v2rayNG settings drawer, with the Routing Settings highlighted.

There are a bunch of these rules already setup, but I also created my own based on a list of domains I'm always surfing: various community-hosted chats and resources. I'm not sure, but I believe this may have helped my v2ray servers to sneak by 100% undetected: They shared IP addresses with other active & recognizable services that were not blocked.

It might have also helped that lot of these IP addresses aren't on mainstrem American cloud providers: Its a mix of small scale colo and residential ISP addresses.

Screenshot of the Default v2rayNG Routing settings page with a new entry called "homebrew" added.  It's "domain" field is set to a comma separated list of domains like cyberia.club, sequentialread.com, as well as various subdomains.  The "outboundTag" setting is set to "direct"

I made sure to add the same list and behavior on the desktop app as well.

At any rate, there's more to life than using the internet. So have some pictures of delicious foods I tried while I was in Shanghai.

lonzhou beef noodle  (my brother told me this is perhaps the most stereotypical or common chinese dish, like a hamburger in america)
spicy pickled carrots and colorful radishes
Chongqing noodles Xiao Mian (noodles served with tons of hot peppers, numbing spice, white beans, and wilted greens)

Comments