Start your 3-day free trial
Sign up to experience all premium features at no cost.
*Available only to new users. Each user is limited to one trial.


An Xray proxy chain sends a selected application's connection through an entry server A and then an exit server B. Build it by checking each hop independently, attaching B's dialer to A, and testing that a failed hop stops the selected request rather than silently sending it directly.
Key Takeaways
- Control both servers and the destination, or obtain explicit permission from their owners.
- A final exit address cannot prove which entry carried the connection.
- Keep authentication, TLS verification, routing, and DNS checks separate.
- This example covers selected TCP applications, not a device-wide tunnel or UDP.
This guide is compiled from official documentation for Xray-core v26.3.27, with an Ubuntu 24.04 LTS command-line client and two already configured, authorized Trojan-over-TLS endpoints. It has not received a complete end-to-end author test. The release pin describes the example, not the latest version or a reason to ignore security updates.[1]
Use the self-hosting responsibility checklist before changing anything. For the trust model, read what two VPN hops can and cannot hide; adding a hop does not establish anonymity, higher speed, or resistance to blocking. Xray and V2Ray's different roles help distinguish a core from an application that manages it.
If you only need an everyday device exit and do not need control of two servers, compare device-wide coverage with an application proxy. That is a different task, not a product-based repair for this chain.
Use a separate client and dedicated test identities. Do not close or change your everyday proxy or VPN client to make the example work. If its existing route affects the test, record that extra layer or use an authorized isolated machine; do not describe the result as a clean two-hop path.
| Role | Required control | Evidence to retain privately |
|---|---|---|
| Client C | Its test process and loopback port | Core version, configuration location, process owner |
| Entry A | Authenticated TLS endpoint | Test identity, source restriction, connection to B |
| Exit B | Authenticated TLS endpoint | Connection from A, permitted destination, egress family |
| Target T | Authorized HTTPS service | Certificate identity and time-correlated request record |
Keep an independent console or management connection that does not use the chain. Record existing listeners, exact firewall owners, DNS settings, and application proxy settings. Back up only files you will edit, with private permissions. If the same process or configuration serves other users, stop and create a separate test instance.
A and B must already have distinct Trojan credentials, valid certificates for their respective names, compatible transport settings, and narrowly permitted traffic. Match inbound and outbound fields using the official protocol documentation.[2] Restrict A to the approved client source where practical, B to A for the chained test, and both servers to authorized destinations. Do not create an anonymous listener, disable a firewall, or borrow an unapproved residential exit.
dialerProxy names the outbound used to establish another outbound's underlying connection. Here the application selects exit-b, while exit-b uses entry-a to reach B; entry-a must not point back to B or itself.[3] Unique tags and an acyclic reference graph are essential.
Create a private working directory as the ordinary test user. Put the following complete client configuration in chain.json. Replace the documentation addresses, certificate names, ports, and both password placeholders with your authorized endpoints; the shown values are not working nodes. Keep passwords out of command-line arguments and shared logs.
{
"log": {"loglevel": "warning"},
"inbounds": [
{
"tag": "lab-socks",
"listen": "127.0.0.1",
"port": 10818,
"protocol": "socks",
"settings": {"auth": "noauth", "udp": false}
}
],
"outbounds": [
{"tag": "deny", "protocol": "blackhole", "settings": {}},
{
"tag": "entry-a",
"protocol": "trojan",
"settings": {
"servers": [{"address": "192.0.2.10", "port": 18443,
"password": "REPLACE_ENTRY_PASSWORD"}]
},
"streamSettings": {
"network": "raw", "security": "tls",
"tlsSettings": {"serverName": "a.example.com", "allowInsecure": false}
}
},
{
"tag": "exit-b",
"protocol": "trojan",
"settings": {
"servers": [{"address": "198.51.100.20", "port": 18443,
"password": "REPLACE_EXIT_PASSWORD"}]
},
"streamSettings": {
"network": "raw", "security": "tls",
"tlsSettings": {"serverName": "b.example.com", "allowInsecure": false},
"sockopt": {"dialerProxy": "entry-a"}
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{"type": "field", "inboundTag": ["lab-socks"],
"network": "tcp", "outboundTag": "exit-b"}
]
}
}
The loopback SOCKS listener is for a trusted single-user client; other local users may also reach it. Do not use this unauthenticated local entry on a shared machine. UDP is explicitly disabled.[4] TLS names must match valid endpoint certificates; this tutorial does not mix REALITY, Vision, multiplexing, or transparent capture into the chain. Transport security and dialer selection are different settings.[2][5]
Routing evaluates rules in order and uses the first outbound when none matches. Putting deny first makes unmatched traffic fail closed; the explicit TCP rule chooses B for the lab entry.[6] Do not add a direct outbound or a fallback balancer. A future direct exception requires its own purpose, scope, and failure evidence.
Run these checks with the intended binary path, substituting it if necessary:
xray version
xray run -test -config ./chain.json
ss -lntp '( sport = :10818 )'
The port must be free before launch. Configuration-test success establishes parsing only: it does not contact both servers, authenticate, or prove the requested path. Stop if the binary version or loaded configuration differs from the recorded pair.
Make two private copies for the single-hop checks. In the A copy, select entry-a in the routing rule. In the B copy, keep the rule on exit-b but remove only B's sockopt.dialerProxy. Run one foreground instance at a time on the reserved port, request T, record the corresponding server evidence, and stop that instance with Ctrl-C before starting another.
A standalone B test must originate from an already authorized source or B's own controlled test environment. If B only permits A, perform the B-only check from A; do not broaden its public access just to run a client-side baseline. The baseline source and expected exit must be written down separately from the final chain.
Use a known HTTPS target you control and verify its TLS certificate normally. This is a reader command, not a reported test result:
curl --fail --show-error --connect-timeout 5 --max-time 15 \
--noproxy "" --proxy socks5h://127.0.0.1:10818 https://target.example.com/health
Replace the target before running it. socks5h passes the destination name to the proxy; it does not capture unrelated system DNS or a browser's own secure-DNS requests. Distinguish connection refusal, TLS failure, invalid credentials, and target denial before composing the hops.
--noproxy "" overrides inherited NO_PROXY/no_proxy exclusions for this command, which would otherwise bypass even an explicit --proxy for matching targets. Keep the override in every single-hop, chained, and failed-hop request; it does not change other applications' proxy settings.[7]
Restore the original chain file, check it again, and launch xray run -config ./chain.json in the foreground. Send a fresh request with the explicit proxy command. Correlate timestamps and the authorized target across C, A, B, and T, allowing for clock differences; a warm connection may hide a new authentication failure.
On A, confirm the approved client identity and an outbound connection toward B. On B, confirm the source corresponding to A and the target request. A's egress can use NAT, so record the expected translated source rather than assuming its interface address appears at B.
For public egress comparison, use a browser profile explicitly configured for this SOCKS entry to inspect the address visible to a website. Expect B's egress for that request. This observation supports only that request; it does not replace per-hop records or prove every application uses B.
Keep logs brief and access-controlled. If you temporarily enable access logging through the instance's documented log configuration, retain only the diagnostic interval and restore the previous logging policy. Do not collect payloads, publish passwords, or treat log absence alone as proof that traffic was blocked.
The table describes reader checks and expected policy, not measured results. Test one change at a time with new connections, then restore the known-good configuration before the next case.
| Check method | Expected observation | If it differs |
|---|---|---|
| Fresh chained TCP request | A and B records correlate with T | Stop; check tags, TLS, and routing |
| Wrong test password at A, then B | Each affected new request fails | Inspect identity selection and unintended routes |
| Stop only the dedicated A instance | New chained requests fail | Look for a direct branch or another proxy layer |
| Restore A; stop only dedicated B | New chained requests fail | Do not accept A-only egress as chain success |
| Use controlled IPv4-only and IPv6-only targets | Each declared supported family reaches T through B | Mark unsupported families; inspect B's route |
| Compare name-based and numeric-target checks | Target DNS path matches the stated resolver design | Check client, A, B, and application resolvers separately |
| Application requires UDP or QUIC | This TCP-only example is insufficient | Use a separately documented and verified design |
Do not stop a shared service for a failure test. Endpoint IP literals in the example avoid bootstrap lookups for A/B, while TLS still checks their names. Target names forwarded by SOCKS need a resolver at the relevant exit path; routing.domainStrategy alone is not a DNS leak-prevention policy.[3][6]
A browser may choose a different protocol or bypass its proxy setting. If a controlled private target fails because addresses collide, first resolve the overlapping-subnet topology; another proxy hop does not identify two machines sharing an address.
Stop only the foreground process you started and confirm its loopback listener has gone. Revoke the dedicated identities on A and B and verify that new authenticated connections with those identities fail; terminate remaining test sessions if the server's revocation mechanism does not close them.
Restore only this test's application proxy setting, temporary logging, and exact firewall exceptions. Remove private configuration copies after retaining the minimal sanitized evidence required by your policy. Never remove a shared configuration directory or flush a firewall to clean up.
Independently read back management access, original listeners, routes, and DNS settings. If restoration cannot be confirmed, stop further experiments and use the recovery console. Keeping the chain requires a new operational decision covering credential rotation, certificate renewal, log retention, and failure behavior.
No. Operators, timing, accounts, and destination identifiers can still link activity; a second hop changes the path without proving anonymity.
It belongs on the outbound for B in this client design, referencing A's outbound tag so B's underlying connection travels through A.
Not for this acceptance policy. Unmatched traffic must be denied, and any separately approved direct branch needs explicit scope and failure checks.
No. That website identifies its observed exit; use correlated A and B evidence to distinguish a chain from a direct connection to B.
No. The listener disables UDP and accepts only explicitly proxied applications; system services and other applications retain their existing network paths.
No. Check the certificate name, trust chain, system time, and endpoint separately; bypassing verification changes the security contract instead of fixing it.
Stop this procedure unless their owners provide the needed authorization and evidence. An unknown third-party endpoint cannot satisfy the stated acceptance checks.
Sources:
Sources checked 11 October 2026.
Related Articles:
Sign up to experience all premium features at no cost.
*Available only to new users. Each user is limited to one trial.





