Xray (VLESS+Reality)

Hello everybody,

I am trying to set up a ProxyVM on Qubes 4.2 to route traffic through a VLESS/REALITY proxy. The ultimate goal is to chain it before an AppVM (or even sys-whonix), so that all traffic from the client qube is transparently tunneled.

Here is my setup:

A StandaloneVM based on the Debian 12 template, named sys-proxy.
sys-proxy has networking set to sys-firewall and "Provides network" is enabled.
xray (latest version) is installed and the systemd service is running.
The sys-proxy VM itself has full network connectivity (I can ping external IPs and domains, and sudo apt update works correctly).

My xray configuration’s inbound is set to dokodemo-door on port 10808. The outbound is a standard VLESS/REALITY configuration that has been tested and works.

My /rw/config/qubes-firewall-user-script contains the following nftables rules to redirect traffic:

#!/bin/bash
nft insert rule ip qubes dnat-dns iifname "vif*" tcp redirect to :10808
nft insert rule ip qubes dnat-dns iifname "vif*" udp dport 53 redirect to :10808

When I set a client qube’s networking to sys-proxy, I lose all internet connectivity (“Unable to connect” in browser).

This indicates the traffic is being redirected, but the connection fails somewhere inside sys-proxy. The xray service remains active and running, and its logs don’t show any obvious errors.

I feel like I’m missing a key piece of how Qubes’ firewall interacts with this kind of traffic redirection. Can anyone see what I’m doing wrong or suggest the correct way to implement a transparent proxy in a ProxyVM?

Thank you.

1 Like

Hello everyone,

Thank you for reading. I wanted to provide a detailed update on my troubleshooting, as the problem has become more specific and perplexing.

Following the advice in some community guides (like the one for using WireGuard as a service qube), I abandoned the dokodemo-door + nftables approach. Instead, I tried a method that seems more robust and better integrated: using sing-box with a tun interface to create a transparent gateway.

Here is a summary of the steps I took and the results:

1. The New Setup:

  • I used a clean StandaloneVM (sys-proxy) based on the Debian 12 template.
  • I downloaded the sing-box binary (v1.9.1) and placed it in the VM.
  • I configured sys-proxy’s Net Qube to point directly to sys-net to avoid any potential conflicts with an intermediary sys-firewall, as recommended by the Qubes documentation for network service qubes.
  • The Provides network flag is enabled for sys-proxy.

2. The sing-box Configuration: My sing-box-config.json is set up to create a tun interface and route all traffic through it to my VLESS/REALITY server. This is the exact configuration:

json

{
  "log": { "level": "info", "timestamp": true },
  "dns": {
    "servers": [{ "tag": "dns-over-proxy", "address": "https://8.8.8.8/dns-query", "detour": "proxy" }],
    "final": "dns-over-proxy",
    "strategy": "ipv4_only"
  },
  "inbounds": [{
    "type": "tun", "tag": "tun-in", "interface_name": "tun0", "inet4_address": "172.19.0.1/30",
    "auto_route": true, "strict_route": true, "stack": "system", "sniff": true
  }],
  "outbounds": [
    {
      "type": "vless", "tag": "proxy", "server": "YOUR_SERVER_IP", "server_port": 443,
      "uuid": "YOUR_UUID", "flow": "xtls-rprx-vision",
      "tls": {
        "enabled": true, "server_name": "www.microsoft.com",
        "utls": { "enabled": true, "fingerprint": "chrome" },
        "reality": { "enabled": true, "public_key": "YOUR_PUBLIC_KEY", "short_id": "YOUR_SHORT_ID" }
      }
    },
    { "type": "dns", "tag": "dns-out" }
  ],
  "route": {
    "rules": [{ "protocol": "dns", "outbound": "dns-out" }],
    "final": "proxy"
  }
}

3. The Result and The Paradox: When I start sing-box with sudo, an interface tun0 is created successfully. However, when I try to access the internet from sys-proxy itself, I get a DNS resolution error: curl: (6) Could not resolve host: ipinfo.io

The sing-box logs show the underlying problem very clearly: ERROR dns: exchange failed for ipinfo.io. IN A: dial tcp YOUR_SERVER_IP:443: i/o timeout

This log indicates that sing-box is correctly trying to establish a VLESS connection to my server to resolve the DNS query, but the connection is timing out.

Here is the part that is completely confusing me: To rule out any network blocks, I stopped sing-box and ran a simple connectivity test from within the same sys-proxy VM: nc -vz YOUR_SERVER_IP 443

The result was an immediate success: Connection to YOUR_SERVER_IP 443 port [tcp/https] succeeded!

4. Conclusion and Question: This proves that my sys-proxy VM has a clear, unblocked network path to my VPS on the required port. A simple TCP connection works perfectly. However, the moment sing-box tries to establish its VLESS (TLS-based) connection to the exact same destination, it fails with a timeout.

My latest hypothesis is that the problem is not within Qubes OS, but on the server-side (e.g., a VPS firewall that drops complex TLS ClientHello packets but allows simple SYN packets). Unfortunately, I’ve been unable to verify this as I’ve run into SSH key issues (Permission denied (publickey)) trying to access the VPS to check its firewall.

Has anyone encountered a situation like this? Where a simple tool like netcat can connect, but a more complex Go application like sing-box or xray cannot? Could this still be a subtle Qubes-specific issue I am missing, or does this strongly point to a server-side firewall configuration?

Any insights would be greatly appreciated.

1 Like

With hijack-dns enabled, add route_exclude_address to tun Inbound:

  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "singtun0",
      "address": [
        "172.18.0.1/30",
        "fdfe:dcba:9876::1/126"
      ],
      "auto_route": true,
      "strict_route": true,
      "route_exclude_address": [
        "10.137.0.0/16",
        "10.138.0.0/16",
        "fd09:24ef:4179::a89:0/112",
        "fd09:24ef:4179::a8a:0/112"
      ]
    }
  ],

And add these firewall rules:

sudo nft add rule ip qubes custom-input iifname singtun0 ip saddr 172.18.0.1/30 accept
sudo nft add rule ip6 qubes custom-input iifname singtun0 ip6 saddr fdfe:dcba:9876::1/126 accept
2 Likes

Example sing-box config, replace CHANGEME strings:

{
  "log": {
    "level": "warn",
    "timestamp": true
  },
  "dns": {
    "servers": [
      {
        "tag": "dns_proxy",
        "address": "https://9.9.9.9/dns-query",
        "detour": "proxy"
      },
      {
        "tag": "dns_direct",
        "address": "local",
        "detour": "direct"
      }
    ],
    "rules": [
      {
        "outbound": "any",
        "server": "dns_direct"
      },
      {
        "rule_set": "geosite-category-CHANGEME",
        "server": "dns_direct"
      }
    ],
    "final": "dns_proxy"
  },
  "inbounds": [
    {
      "type": "tun",
      "tag": "tun-in",
      "interface_name": "singtun0",
      "address": [
        "172.18.0.1/30",
        "fdfe:dcba:9876::1/126"
      ],
      "auto_route": true,
      "strict_route": true,
      "route_exclude_address": [
        "10.137.0.0/16",
        "10.138.0.0/16",
        "fd09:24ef:4179::a89:0/112",
        "fd09:24ef:4179::a8a:0/112"
      ]
    }
  ],
  "outbounds": [
    {
      "type": "vless",
      "tag": "proxy",
      "server": "CHANGEME",
      "server_port": 443,
      "uuid": "CHANGEME",
      "flow": "xtls-rprx-vision",
      "tls": {
        "enabled": true,
        "server_name": "CHANGEME",
        "utls": {
          "enabled": true,
          "fingerprint": "chrome"
        },
        "reality": {
          "enabled": true,
          "public_key": "CHANGEME",
          "short_id": "CHANGEME"
        }
      },
      "multiplex": {
        "protocol": "h2mux",
        "max_streams": 32,
        "padding": true,
        "brutal": {
          "up_mbps": 1000,
          "down_mbps": 1000
        }
      },
      "packet_encoding": "xudp",
      "tcp_fast_open": true
    },
    {
      "type": "direct",
      "tag": "direct"
    }
  ],
  "route": {
    "rules": [
      {
        "action": "sniff"
      },
      {
        "protocol": "dns",
        "action": "hijack-dns"
      },
      {
        "type": "logical",
        "mode": "or",
        "rules": [
          {
            "port": 853
          },
          {
            "network": "udp",
            "port": 443
          },
          {
            "protocol": "stun"
          }
        ],
        "action": "reject",
        "method": "default"
      },
      {
        "rule_set": [
          "geoip-CHANGEME",
          "geosite-category-CHANGEME"
        ],
        "outbound": "direct"
      },
      {
        "ip_is_private": true,
        "outbound": "direct"
      }
    ],
    "rule_set": [
      {
        "type": "remote",
        "tag": "geoip-CHANGEME",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-CHANGEME.srs",
        "download_detour": "proxy"
      },
      {
        "type": "remote",
        "tag": "geosite-category-CHANGEME",
        "format": "binary",
        "url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-CHANGEME.srs",
        "download_detour": "proxy"
      }
    ],
    "final": "proxy",
    "auto_detect_interface": true
  }
}
2 Likes

Will try it. Thank you!

1 Like