Build and Verify an Xray Proxy Chain Hop by Hop

Build and Verify an Xray Proxy Chain Hop by Hop

Ryan Foster
October 11, 2026· 11 min read

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.

What does an Xray proxy chain cover?

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.

Step 1: Reserve the topology and the recovery path

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.

RoleRequired controlEvidence to retain privately
Client CIts test process and loopback portCore version, configuration location, process owner
Entry AAuthenticated TLS endpointTest identity, source restriction, connection to B
Exit BAuthenticated TLS endpointConnection from A, permitted destination, egress family
Target TAuthorized HTTPS serviceCertificate 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.

Step 2: Check Xray dialerProxy and configuration ownership

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.

Step 3: Verify proxy chain components independently

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]

Step 4: Compose the hops and compare the evidence

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.

Step 5: Test DNS, address families, and failed hops

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 methodExpected observationIf it differs
Fresh chained TCP requestA and B records correlate with TStop; check tags, TLS, and routing
Wrong test password at A, then BEach affected new request failsInspect identity selection and unintended routes
Stop only the dedicated A instanceNew chained requests failLook for a direct branch or another proxy layer
Restore A; stop only dedicated BNew chained requests failDo not accept A-only egress as chain success
Use controlled IPv4-only and IPv6-only targetsEach declared supported family reaches T through BMark unsupported families; inspect B's route
Compare name-based and numeric-target checksTarget DNS path matches the stated resolver designCheck client, A, B, and application resolvers separately
Application requires UDP or QUICThis TCP-only example is insufficientUse 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.

Step 6: Revoke the test and restore the baseline

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.

Summary

  • Prove each hop before using the chained dialer.
  • Combine per-hop records with the final exit observation.
  • Declare TCP, DNS, and address-family limits explicitly.
  • Treat unexpected direct egress as a failed acceptance check.

FAQ

Is two-hop access automatically anonymous?

No. Operators, timing, accounts, and destination identifiers can still link activity; a second hop changes the path without proving anonymity.

Does dialerProxy belong on entry A or exit B?

It belongs on the outbound for B in this client design, referencing A's outbound tag so B's underlying connection travels through A.

Can I leave a direct outbound as the default?

Not for this acceptance policy. Unmatched traffic must be denied, and any separately approved direct branch needs explicit scope and failure checks.

Does one website showing B prove the Xray proxy chain?

No. That website identifies its observed exit; use correlated A and B evidence to distinguish a chain from a direct connection to B.

Does this configuration cover UDP and the whole device?

No. The listener disables UDP and accepts only explicitly proxied applications; system services and other applications retain their existing network paths.

Should I disable certificate verification to diagnose a failure?

No. Check the certificate name, trust chain, system time, and endpoint separately; bypassing verification changes the security contract instead of fixing it.

What if I cannot control both hops?

Stop this procedure unless their owners provide the needed authorization and evidence. An unknown third-party endpoint cannot satisfy the stated acceptance checks.

Sources:

  1. Xray-core — Release v26.3.27
  2. Project X — Trojan outbound and credential fields
  3. Project X — Sockopt
  4. Project X — Socks inbound
  5. Project X — Transport Configuration
  6. Project X — Routing
  7. curl — --noproxy and proxy environment variables

Sources checked 11 October 2026.


Related Articles:

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.

Build and Verify an Xray Proxy Chain Hop by Hop | AethoVPN