Your Own Proxy Against DPI: an Android Client
In the previous article we set up an Xray server with VLESS, XTLS Vision and REALITY, and a v2rayN client on Windows. Here is the same for Android. A phone visits other people's networks more often than any other device: mobile internet, café Wi-Fi, roaming. That is exactly where a proxy is needed most.
In the article: how a VPN works in Android and why there is only one of them in the system; what is inside the v2rayNG app; how to install it with a signature check; how to give the phone its own key and enter it with a QR code; which settings to change and why — IPv6, routeOnly, DNS, bypassing the local network; how routing works and what to do with rules labelled in Chinese; how to check the result in the server log, and which pitfalls wait when you switch between VPNs and between networks.
The server and the protocols are not explained again here: they are in the previous article, and DNS, TCP, UDP and TLS have parts in the "Networks for Beginners" series. What you need to follow along is repeated in a sentence or two.
The addresses in the article are documentation ones: the server is 203.0.113.10 and 2001:db8::10, the masking site is www.example.com, the home network is 192.168.1.0/24 and 2001:db8:1::/64, and the phone reaches the internet from 198.51.100.23. Keys are placeholders in angle brackets. The client version is v2rayNG 2.3.11 (with Xray-core 26.10.10 inside), Android 7.0 and newer; everything described was checked on a Xiaomi 13T Pro running Android 16. Menu items are given as they are named in the English interface.
How a VPN works in Android
An app cannot intercept other apps' traffic on its own. Android has a system interface for this, VpnService. The order is:
- The app asks the system for permission. The system shows a "Connection request" window: the app wants to set up a VPN, and all network traffic can be intercepted. Nothing happens further without your consent.
- After you agree, the app describes a network interface through
VpnService.Builder: its address (addAddress), routes (addRoute), DNS server addresses (addDnsServer), MTU (setMtu) and, if needed, a list of apps that go into the tunnel or stay out of it (addAllowedApplication,addDisallowedApplication). - The
establish()method creates a virtual TUN interface in the system and gives the app a file descriptor. TUN is an interface that hands IP packets to a program instead of putting them on a wire: everything the system sends along the tunnel's routes the program reads from the descriptor as raw packets, and the packets it writes into it the system accepts as if they had arrived from the network. - The app does what it wants with the packets: encrypts them and sends them to its server, filters them, counts them. For that, its own connection to the server has to go around the tunnel, otherwise packets carrying packets would loop forever. So the app either marks a socket as "outside the VPN" (
protect) or excludes itself from the tunnel, and v2rayNG does the second: at startup its code callsaddDisallowedApplicationwith its own package name.
Two consequences matter in practice.
There is one VPN in the system. Android runs only one VpnService at a time: when another app calls establish(), the previous one is revoked. A key icon appears in the status bar. If the phone has a home IKEv2, WireGuard and v2rayNG, you can run any one of them, and starting the next one disables the previous one. This is not a bug, it is by design.
Routes decide what goes into the tunnel. Whatever does not match any of the tunnel's routes goes out to the network as usual. This will matter below: whether, for example, IPv6 ends up in the tunnel depends on which routes v2rayNG added.
What is inside v2rayNG
v2rayNG is an open-source app from the author of v2rayN (the client we installed on Windows). The shell is written in Kotlin, and two programs inside it do the networking:
- Xray-core — the same core as on the server (in the app it is linked in as a Go library). It accepts connections on a local port, decides by the routing rules where to send them, and knows how to go out through VLESS with REALITY.
- hev-socks5-tunnel — a C program that reads packets from TUN. Programs like this are called tun2socks. It has its own user-space implementation of the TCP/IP stack: it assembles TCP connections and UDP datagrams from IP packets and, for each of them, makes a request to an ordinary SOCKS5 proxy. In v2rayNG this is on by default — the Use Hev TUN item. If you turn it off, the Xray core itself reads the packets from TUN.
SOCKS5 (RFC 1928) is a simple proxy protocol: the client tells the proxy the destination address, and then a stream follows. For TCP that is the CONNECT command. For UDP there is UDP ASSOCIATE: the client gets from the proxy an address to which it can send datagrams, each carrying the destination address in its header. The hev-socks5-tunnel configuration in the app shows exactly this: the proxy is 127.0.0.1:10808, the UDP mode is udp, that is, via UDP ASSOCIATE. Port 10808 is listened on by Xray-core, whose input is SOCKS5.
Android apps
│ IP packets (the system picks them by the VPN routes)
▼
TUN interface ────► hev-socks5-tunnel ──► SOCKS5 127.0.0.1:10808
│
Xray-core: sniffing + rules
┌───────────────┼───────────────┐
▼ ▼ ▼
proxy direct block
VLESS + Vision + REALITY bypass proxy drop
│
▼
Xray server ────► internet
The core has three exits, the same as in v2rayN: proxy — send to the server via VLESS and REALITY, direct — send straight from the phone (the core itself is excluded from the tunnel, so there is no loop), block — drop.
UDP works the same way as in the previous article: every datagram travels inside the same TCP connection to the server (the XUDP multiplexer), is unpacked there, and goes out to the internet as real UDP. In the server log you can see it: for UDP entries the source is marked tcp: — the UDP came over TCP. A sample is below, in "Hands-on".
Installation
The project distributes the app on the GitHub releases page: github.com/2dust/v2rayNG/releases. The project description lists no other official sources. App stores may contain clones with similar names: install only from here. Take the latest stable release (on the page it has no "Pre-release" label; on 23 October 2026 that is 2.3.11 from 11 October, and versions 2.3.12 and 2.3.13 are marked as pre-releases).
There are many files in a release. For most modern phones you need v2rayNG_2.3.11_arm64-v8a.apk:
| File | What it is for |
|---|---|
…_arm64-v8a.apk |
64-bit ARM processors — nearly all phones of recent years |
…_armeabi-v7a.apk |
old and budget 32-bit phones |
…_universal.apk |
any processor, the file is about twice as large |
…_x86.apk, …_x86_64.apk |
emulators and rare tablets |
…-fdroid_….apk |
the same app under a different package name (an F-Droid catalog requirement); an ordinary user does not need it |
The minimum Android version is 7.0. If you do not know your phone's architecture, take universal.
Checking the file
Next to the APK there is its signature ….apk.sig and the author's public key v2rayN-public-key.asc, and the release page shows the SHA-256 of every file. SHA-256 is a checksum: change one byte of the file and it comes out completely different. A PGP signature is a different protection: the author signs the file with a private key, and anyone holding the public key can check that the file was signed by exactly that person and has not been modified. It is more reliable to check on a computer, before the file reaches the phone:
sha256sum v2rayNG_2.3.11_arm64-v8a.apk
b9aed943bdace5531b2268eb57479db769fffa5463adecb1559c8c62d5864b6f v2rayNG_2.3.11_arm64-v8a.apk
gpg --import v2rayN-public-key.asc
gpg --verify v2rayNG_2.3.11_arm64-v8a.apk.sig v2rayNG_2.3.11_arm64-v8a.apk
The gpg output should contain the line Good signature from "2dust <2dust-noreply@github.com>" and the key fingerprint:
Primary key fingerprint: 76945 E9F3E 9A168 F8070 F1958 05D66 1C134 DFAF6 8903C 19946
The warning This key is not certified with a trusted signature is normal: gpg does not know the key belongs to this author until you certify it yourself. Compare the fingerprint with what you see on the project page or from another trustworthy source. In PowerShell the same SHA-256 is computed like this:
Get-FileHash .\v2rayNG_2.3.11_arm64-v8a.apk -Algorithm SHA256
If checking on a computer is inconvenient, download the file directly on the phone from the release page: GitHub serves it over HTTPS, and on updates Android will in any case refuse to install over the app a file signed with someone else's key.
Installing on the phone
- Download the APK in the phone's browser (or transfer it from the computer).
- Android will ask for the "Install unknown apps" permission for the browser or file manager you open the file from. Allow it. After installation you can revoke it in the settings.
- Play Protect may offer to scan the app: that is normal for files from outside the store.
- First launch: the app may ask for permission to show notifications. Allow it: the notification keeps the VPN service in a working state, and the system stops background services without it more readily.
The app is updated manually: side menu → Check for update; there is a switch that allows pre-release versions. Do not install them without need.
A key for the phone
In the previous article we created the xray-user script on the server. The phone gets its own user, just like the laptop: if you lose the phone you revoke only its key, and the other devices do not notice.
xray-user add alice-phone
The script prints two vless://… links: one with the server's IPv4 address and one with IPv6. A link is a secret, it contains the UUID. The device must receive it without copies left in chats and mail. The most convenient way is a QR code that encodes the same string:
apt install qrencode
xray-user link alice-phone | head -n1 | tr -d '\n' | qrencode -t PNG -s 9 -o alice-phone.png
xray-user link prints the links of an existing user; head -n1 takes the first one, with IPv4, and tr -d '\n' removes the newline so that no extra character gets into the code. Copy the file to a computer, open it on screen, scan it with the phone and delete it: it is a key in the form of a picture. Do not send it to messengers and do not photograph the screen with the code. Instead of a file you can print the code right in the terminal: qrencode -t ansiutf8; on a dark terminal theme the inverted variant -t ansiutf8i is sometimes needed, and if the phone cannot read the code, enlarge the terminal font.
First connection
- In v2rayNG tap + ("Add configuration"), choose Import from QR code and allow access to the camera. If you have no code, copy the link and choose Import from Clipboard.
- A "Success" message appears and the profile shows up in the list. The app takes its name from the end of the link after the
#sign; for us it isalice-phone-ipv4. - Tap the profile to select it, then the start button. Android shows the VPN request window — confirm. A key appears in the status bar and the status in the notification.
- The status at the bottom of the screen reads "Connected. Tap to check connection". Tap it: the app sends a request through the tunnel to
https://www.gstatic.com/generate_204and shows the response time, "Connection succeeded in …". The check address is changed in Settings → Advanced Settings → Real delay test URL.
The profile's parameters are visible if you choose the edit item: server address and port, id (the UUID), flow (xtls-rprx-vision), encryption (none), TLS = reality, SNI, Fingerprint (chrome), PublicKey, ShortId. These are the same parameters as in the table about the vless:// link in the previous article, and you do not need to change them by hand.
After the first connection the app already works, but with "out of the box" settings that are not optimal for our setup. Next is what exactly to change and why.
Settings: what to change and why
All settings are on one scrollable Settings page split into collapsible groups: UI settings, VPN Settings, Core Settings, Mux Settings, Fragment Settings, Observatory Settings, Advanced Settings and Mode Settings. Below is only what matters for our setup.
Operating mode
In Mode Settings → Mode there are two options:
- VPN (default) — the app creates TUN and passes all traffic through it, like the TUN mode of v2rayN on Windows. All apps work, including those that know nothing about proxies, and so does UDP.
- Proxy only — no TUN is created and the VPN slot stays free. The app only listens on a local proxy
127.0.0.1:10808(the core accepts both SOCKS5 and HTTP on this port), and traffic goes into it from those programs that have its address set by hand: some browsers, Telegram. The rest connect directly. This is handy when the VPN slot is needed by another app, for example the home IKEv2.
Everything further is about VPN mode.
IPv6
VPN Settings → Enable IPv6 is off by default. What that means can be seen from how v2rayNG configures VpnService.Builder:
- IPv4: the interface gets an address, and routes to the public IPv4 ranges are added (about bypassing the local network, below).
- IPv6: only if the item is on, the interface gets an IPv6 address and the routes
2000::/3(all globally allocated IPv6 today) andfc00::/18. If it is off, there is no IPv6 in the tunnel at all.
The system chooses a route by the most specific match. Without IPv6 routes in the tunnel, a packet to a site's IPv6 address does not reach the tunnel and goes out to the network as usual, bypassing the proxy. On home Wi-Fi the router hands the phone a real global IPv6 address by SLAAC — automatic address configuration from the router's announcements, without a DHCP server (part 15) — and every site reachable over IPv6 sees your home address rather than the server's. A classic "IPv6 leak". Mobile networks with IPv6 are the same. Turn the item on.
Next to it is Prefer IPv6: when resolving names, the app prefers IPv6 addresses. It is not required: apps with IPv6 cope on their own.
Sniffing and routeOnly
Core Settings → Enable Sniffing is on by default, and Enable routeOnly is off. It is the same trap as on Windows. The core pulls the site name out of the first bytes of a connection (from the SNI in the TLS ClientHello or from the Host header) and by default replaces the destination address with it: what goes to the server is not "connect to this IPv6 address" but "connect to example.com", and the server resolves the name itself and picks IPv4. Turning on IPv6 in the tunnel does not help with that: the addresses the app chose turn into names.
With routeOnly the name is used only by the routing rules, and the connection goes to the address the app chose. Turn routeOnly on and leave Enable Sniffing on.
The effect is visible in the server log. In my run, before turning both items on, 109 of the phone's 206 entries had a name as the destination and there were no IPv6 addresses at all. After turning them on, only 2 of 111 entries had names, and 33 were IPv6 addresses.
Bypassing the local network
VPN Settings → Bypass LAN in VPN mode takes the values "Follow config", "Bypass" and "Not Bypass". The default is Bypass, and that is what you want. What it does: instead of the route 0.0.0.0/0 the app adds to the tunnel a list of public IPv4 ranges from which three private networks are excluded — 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. Packets to a printer, router or NAS on the home network do not enter the tunnel and go as usual. Without this they would be sent to the server, which (as we configured in the previous article) sends private addresses to blackhole.
There is a caveat about IPv6 in this mode; it is covered in the routing section.
DNS
DNS in the app is set by three settings that are easy to confuse.
| Item | What it is | Default |
|---|---|---|
| VPN DNS (in VPN Settings) | the DNS server addresses the app reports to the system through VpnService.Builder; Android sends queries to exactly these |
1.1.1.1 |
| Remote DNS (in Core Settings) | a resolver inside the Xray core for names that go through the proxy; works only if Enable local DNS is on; an address like https://…/dns-query is DoH, DNS inside HTTPS (RFC 8484) |
https://cloudflare-dns.com/dns-query |
| Direct DNS | the core's resolver for names that go directly; also only with Enable local DNS | 223.5.5.5 |
With the default settings Enable local DNS is off, and only VPN DNS is in play. Android sends a DNS query from an app to 1.1.1.1 over UDP on port 53, the packet enters the tunnel and travels to the server as an ordinary UDP datagram. The server sends it on to 1.1.1.1 from its own address. The phone's provider sees only TLS to your server. If you leave it as is, DNS does not leak.
Two remarks. Direct DNS defaults to the Chinese public resolver 223.5.5.5, just like in v2rayN. It matters only if you turn on local DNS and set up direct routes; then replace it with 1.1.1.1 or your operator's resolver. And leave Enable FakeDNS off: apps get fake addresses instead of real ones, and that is extra complexity.
There is one more place where the system itself gets involved in DNS: Private DNS (in Android settings, network and internet). In "Automatic" mode Android tries DNS over TLS (RFC 7858), that is, ordinary DNS inside TLS on port 853, to the DNS server assigned to the network. In our case that is the VPN DNS 1.1.1.1, and in the server log you can see tcp:1.1.1.1:853 connections from the phone: DoT also goes into the tunnel and on from the server to Cloudflare. There is nothing to change.
The other VPN Settings items
- Add HTTP proxy to VPN: on Android 10 and newer it adds the system HTTP proxy
127.0.0.1to the VPN configuration. Browsers and apps that read it go to the proxy directly, bypassing TUN. It is not necessary to turn it on: it affects only apps that read the system proxy, and it does not touch UDP or the other programs. - VPN MTU (1500) and VPN Interface Address: leave as they are.
- Enable Mux: leave it off. Multiplexing is packing several connections into one. For a profile with a
flow(Vision for us) the app disables it for TCP anyway and keeps only XUDP for UDP, so the item gives nothing.
Routing
Menu → Routing Settings. The list of rules has the same form as in v2rayN: conditions "such addresses or names → such exit", top to bottom, and the first match decides. A rule's fields are domain, ip, port, protocol, network and the exit outboundTag (proxy, direct, block). All filled-in conditions of a rule must be met at the same time. If no rule matches, the traffic goes to proxy.
The default set
On first launch the app loads the "China Whitelist" set: block UDP 443, geosite:google through the proxy, private addresses and domains directly, then directly the public DNS servers of China, and Chinese addresses and domains (geoip:cn, geosite:cn). Everything else goes to the proxy. For our purposes it works, but we do not need the Chinese rules.
Other sets are chosen in the menu: Routing Settings → Import predefined rulesets: "China Blacklist", "Global", "Russia Whitelist", "Iran Whitelist". A set replaces the current list entirely. Global suits us.
What is in "Global" and how to read it
The rules in "Global" have their names written in Chinese:
| Name | Translation | Condition | Exit |
|---|---|---|---|
| 阻断udp443 | Block UDP 443 | port 443, network UDP | block |
| 绕过局域网IP | Bypass the proxy: local IPs | geoip:private |
direct |
| 绕过局域网域名 | Bypass the proxy: local names | geosite:private |
direct |
| 最终代理 | Everything else through the proxy | port 0-65535 |
proxy |
The names (remarks) do not affect anything, they are just labels. You can open a rule and rename it: the "Remarks" field in the Routing Rule Settings window, which is mandatory. In the rule list a short condition is shown under the name (udp443, geoip, geosite, 0-65535), and the exit is visible if you open the rule. The first rule is needed for the same reason as on Windows: UDP to port 443 is QUIC (HTTP/3), and it is better for the browser to switch straight to HTTPS over TCP, where Vision works.
You can enter your own rules by hand: the + button in the list ("Add rule"), or import from the clipboard (the list menu → Import ruleset from clipboard) the same JSON as in v2rayN: an array of objects with the same fields.
The local network over IPv6
Here is what turned up during testing. The geoip:private rule sends 192.168.… addresses and the like directly. But at home, devices' IPv6 addresses are public: the provider gives the router a prefix, for example 2001:db8:1::/64, and the computer, printer and phone get addresses from it. To geoip:private these are ordinary internet addresses. And with IPv6 in the tunnel enabled, the route 2000::/3 intercepts them too.
In the server log it looked like this: the phone sent UDP to the home computer at its IPv6 address 2001:db8:1::10, the request went through the tunnel to the server, and the server tries to reach your home from the outside. The background link between Windows and the phone does not work, and the server sees the addresses and ports of your home devices.
The fix is a rule: send your own prefix directly. In Routing Settings tap + and fill in:
| Field | Value |
|---|---|
| Remarks | Home IPv6 direct |
| ip | 2001:db8:1::/64 (your prefix, or, if the provider delegated a larger block, the whole block, for example a /56) |
| outboundTag | direct |
The new rule goes to the top of the list, and rules are checked from top to bottom, so it fires before the last "everything else goes to the proxy". The packet still enters TUN, but the core sends it directly from the phone, bypassing the server. I did not enter this rule on my own phone: I saw the problem in the server log, and the fix follows from the order in which rules are checked.
One more effect, which cannot be fixed. As long as DNS goes through the proxy, internal names of the home network are resolved by public DNS: the name of your service nas.example.net, which at home has the address 192.168.1.12, may point outside to the router's external address. The request then goes to the external address through the server rather than over the local network. If the service is published to the outside it opens, only by a roundabout route; if not, it does not open at all. Open internal resources by IP address, or switch the tunnel off for a while.
Apps: who goes into the tunnel
Menu → Per-app settings. The Enable per-app item turns the list on, and the Bypass Mode switch chooses what the list means:
- off — only the checked apps go into the tunnel, the others connect directly;
- on — the checked ones connect directly, and everything else goes into the tunnel.
What it is for: banking apps, government services and services that refuse to work with a VPN or cut off foreign addresses; apps that need the home network. The system implements it: apps in the "direct" list are excluded from the VPN network at the VpnService.Builder level, and their packets never reach TUN. v2rayNG itself is always excluded.
Everything else on the Android side
- Other VPNs. When v2rayNG starts, the home IKEv2 and WireGuard are disconnected. Switch by hand: stop one, start the other. If you need both a tunnel into the home network and DPI circumvention, use the Proxy only mode and set the proxy address in the apps that need the circumvention.
- Always-on VPN and "Block connections without VPN": in Android settings (VPN → the gear icon by the profile); v2rayNG has a shortcut: Settings → Advanced Settings → System VPN settings. With the block on, the phone does not go online until the tunnel is up, so nothing leaves around it. I did not turn this on and did not test it for the article: turn it on only after you are sure the connection is stable, otherwise you lose the internet entirely.
- Autostart. Advanced Settings → Connect after device boot: after the phone reboots the app raises the tunnel by itself. Android restricts background starts, and on some phones this works unreliably.
- Battery. Turn off battery optimization for v2rayNG (Settings → Apps → v2rayNG → Battery → "No restrictions"), otherwise the system may stop the service in the background. Many manufacturers (Xiaomi, Huawei, Samsung and others) add their own restrictions on background work to stock Android: in the app's settings look for items such as "Autostart" and "Battery saver". The app's notification must stay.
- Quick switching. The app has a Quick Settings tile and a "Switch" widget: they turn the chosen profile on and off without opening the app.
Hands-on
What the server sees
The Xray access log on the server (it is turned on temporarily, see the "Hands-on" section of the previous article) writes every connection with the user's label. The first minute of the phone's work:
2026/10/23 18:51:24 from tcp:198.51.100.23:46706 accepted udp:1.1.1.1:53 [vless-reality >> direct] email: alice-phone
2026/10/23 18:51:55 from 198.51.100.23:57908 accepted tcp:siteicon.operacdn.com:443 [vless-reality >> direct] email: alice-phone
2026/10/23 18:51:57 from 198.51.100.23:45802 accepted tcp:ip6.me:443 [vless-reality >> direct] email: alice-phone
What to read in a line: from — the address the phone came from (for us it is the home router's external address; on a mobile network it will be the operator's address); accepted tcp:… or accepted udp:… — where the connection was sent; [vless-reality >> direct] — the input and output of the core on the server; email — the user's label. For UDP entries the source is marked tcp:: the datagram arrived inside a TCP connection, as said above. Entries with a name as the destination (siteicon.operacdn.com) are from before routeOnly was turned on; after it, addresses dominate:
2026/10/23 19:04:54 from 198.51.100.23:55010 accepted tcp:1.1.1.1:853 [vless-reality >> direct] email: alice-phone
2026/10/23 19:04:56 from 198.51.100.23:56714 accepted tcp:[2a00:1450:4001:c17::66]:443 [vless-reality >> direct] email: alice-phone
The first line is Private DNS (DNS over TLS on port 853), the second is the first connection to a site's IPv6 address through the tunnel.
The access log records the addresses each user goes to. Turn it on only for debugging and at the right moment, and then return "access": "none".
Statistics without a log
You can find out whether a user is connected and how much they transferred without recording destination addresses at all. For that, three sections are added to the server config (next to log, inbounds and the rest):
"stats": {},
"api": { "tag": "api", "listen": "127.0.0.1:10085", "services": [ "StatsService" ] },
"policy": {
"levels": { "0": { "statsUserUplink": true, "statsUserDownlink": true, "statsUserOnline": true } }
}
stats turns on the counters, policy says which ones to count (level 0 is the default level of users), and api opens a gRPC management interface on the server's local address, not reachable from outside. Check and restart as usual:
xray run -test -config /usr/local/etc/xray/config.json
systemctl restart xray
Now you can ask who is connected and from where, and how much traffic has passed:
xray api statsonlineiplist --server=127.0.0.1:10085 -email alice-phone
xray api statsquery --server=127.0.0.1:10085 -pattern 'user>>>alice-phone'
{ "ips": { "198.51.100.23": 1792781523 }, "name": "user>>>alice-phone>>>online" }
"name": "user>>>alice-phone>>>traffic>>>uplink", "value": 123604
"name": "user>>>alice-phone>>>traffic>>>downlink", "value": 132163
The first command shows the addresses the user is connected from now and the time of last activity (the number is a timestamp in seconds). The counters live until Xray restarts. If the user has not connected since the start, the answer is not found.
The exit address
On the phone, with the tunnel on, open ifconfig.co and ip6.me in the browser: they should show the server's addresses, 203.0.113.10 and 2001:db8::10, and not your home ones. In my case, after setup both sites showed the server's addresses, both IPv4 and IPv6. If IPv6 shows a home address, the Enable IPv6 item is not on.
Switching networks
A phone moves between Wi-Fi and the mobile network all the time, and the tunnel has to survive it. The connection to the server is plain TCP, and TCP is tied to a pair of addresses and cannot "move" to another one (unlike IKEv2 with the MOBIKE extension, which lets a VPN session move to a new address). When the network changes, the connections to the server break and the app opens new ones. The test: I turned Wi-Fi off with the tunnel on, leaving mobile data, and opened a site — everything worked. The log shows what it looks like on the server:
last record from the home address 19:12:47
first record from the operator's address 19:12:56
About nine seconds, and after that statsonlineiplist showed both addresses, the home one and the operator's (how long the old one stays in the list I did not check). Connections that were open at the moment of the network change break, and apps have to repeat their requests. In the error log during those seconds there are only entries about the server's connections to sites being cut (connection ends, readfrom tcp) — the old connections are closing, and that is normal.
If the operator's mobile network gives only IPv6 (such networks exist), use the second link, with the server's IPv6 address: xray-user link alice-phone prints it on the second line.
If it does not connect
The app itself answers "Connection test failed" with a reason, and the details are in Logcat (a menu item; the detail level is set in Core Settings → Log Level). The server side is visible in the Xray error log:
On the server (error.log) |
Cause |
|---|---|
REALITY: processed invalid connection from …: failed to read client hello |
not TLS arrived: a scanner or a client with another protocol |
REALITY: processed invalid connection from …: server name mismatch |
the client's SNI is not in the serverNames list: the profile has the wrong masking site name |
| the connection is accepted but there are no entries in the access log | wrong UUID: the key was revoked or copied with an error |
From the first two lines you can see that REALITY answers strangers: such entries arrive all the time from internet scanners, and they are handed the real masking site. That is normal operation, not a fault.
Limitations
- A link is a key. Whoever gets
vless://…connects in your name, and the server distinguishes them from you only by address. Do not send links and QR codes to messengers and do not leave them in history. If the phone's link leaks, revoke it withxray-user del alice-phone; the other users are not affected. - One device, one user. Then you can see what each device does, and revocation is targeted.
- Apps that dislike VPNs. Some banks and services detect a VPN network by a system flag and refuse to work. For them there is the Per-app settings list.
- Versions. Keep the server and the client on versions from the same period: Xray and v2rayNG are updated often, and the format and defaults sometimes change. Client 2.3.11 with core 26.10.10 works fine with server 26.3.27, but update both.
- Address. As before, the camouflage does not protect from the server being blocked by IP.
Summary
The phone got what the computer has: all its traffic, including UDP and IPv6, goes to the server inside what looks like ordinary HTTPS to www.example.com. Along the way, with defaults, the app leaves a few things outside the tunnel or substitutes them: IPv6, destination addresses, home IPv6 addresses, Chinese rule sets. All of it is visible in the server log and fixed with a few switches. The main things to remember: Android has one VPN, each device has its own key, and the result should be checked by what the server sees, not by the key icon in the status bar.
Sources
- v2rayNG — the app, the releases page and the description of the files.
- hev-socks5-tunnel — the tun2socks that reads packets from TUN.
- VpnService and VpnService.Builder — how an app creates a VPN in Android; VPN overview.
- Xray-core, REALITY; the Xray reference: routing, statistics, policy, API.
- RFC 1928 — SOCKS5; RFC 7858 — DNS over TLS; RFC 8484 — DNS over HTTPS.
- qrencode — a QR code utility.
Only registered users can leave a comment.
Sign in or register to leave a comment.