Skip to content

Coming from Xray

etemenanki-app follows Xray’s model: inbounds accept clients, a router picks an outbound for each flow, and outbounds carry the traffic on. Most protocols speak the same wire format as Xray, so Xray clients can connect to an etemenanki-app server and the reverse. The configuration file is a different language, though. It is TOML instead of JSON, the keys are snake_case, several nested Xray objects become flat keys, and a number of Xray features are absent on purpose.

This page is for you if you have a working Xray configuration and want to run the same service on etemenanki-app. It maps every common Xray shape to its TOML equivalent, points out the defaults that differ, lists what has no equivalent, and shows which combinations are tested against a real Xray binary. katana, the panel node agent built on the same kernel, takes its node settings from the panel instead of a file, so this page applies to etemenanki-app only.

Xray JSON etemenanki-app TOML Notes
log [log] with one key, level Logs go to standard output. There is no access log.
inbounds[] [[inbound]] One array-of-tables entry per inbound.
outbounds[] [[outbound]] The first outbound is the default route, as in Xray.
inbounds[].streamSettings [inbound.stream] Also [outbound.stream]. Four networks: tcp, tls, ws, grpc.
inbounds[].sniffing sniffing = true on the inbound A boolean. On by default, and it never rewrites the destination.
routing.rules[] [[route.rule]] First match wins, but the conditions inside one rule are OR, not AND.
routing.domainStrategy None The router never resolves names. It behaves like "AsIs".
routing.balancers[] [[balancer]], top level Health comes from a built-in TCP probe, not from observatory.
dns [dns] One upstream resolver, no per-domain servers, no hosts.
policy None Timeouts are fixed. The only connection limits you can set are on hysteria2 and tun inbounds. See Limits.
observatory, burstObservatory None Each balancer probes its own members.
stats, api, metrics None No traffic counters and no control API.
fakedns, reverse, transport None
Several config files, -confdir One TOML file, passed with -c Without -c, config.toml in the working directory is read.
xray run -test -c config.json etemenanki-app --test -c config.toml Checks everything, reads every referenced file, binds nothing.

Every table in the TOML file rejects unknown keys. A key copied over from Xray without translation stops the configuration with an error that names the key and lists the accepted ones, instead of being ignored. The only exceptions are [outbound.settings] on freedom and blackhole, described below.

Most translation mistakes produce an error from --test. These do not, or they produce one that is easy to misread. Check each of them before you switch traffic over.

  1. Inbounds listen on loopback by default. Xray binds 0.0.0.0 when listen is missing. etemenanki-app binds 127.0.0.1, so a server that must be reachable from outside needs listen = "0.0.0.0" (or a specific address) written out.

  2. TLS over TCP is network = "tls". Xray’s "network": "tcp" with "security": "tls" is refused, not translated. security = "tls" is only for adding TLS under ws or grpc.

  3. The conditions in one rule are OR. Xray matches a rule only when every field matches (domain AND port AND network). etemenanki-app matches a rule when any single matcher matches. A rule with network = "udp" and port = ["443"] catches all UDP and all port 443 traffic, not just QUIC. See Rules are OR, not AND.

  4. Sniffing is on by default and only affects routing. Xray sniffs only when sniffing.enabled is true and, unless routeOnly is set, rewrites the destination to the sniffed name. etemenanki-app sniffs every IP-addressed TCP flow unless you set sniffing = false, and uses the name for routing only, which is what Xray does with "routeOnly": true.

  5. freedom and blackhole ignore their settings. These two outbounds never read [outbound.settings], so Xray’s domainStrategy, redirect, fragment and response pass --test there and do nothing. Use address_family on the outbound instead of domainStrategy; the others have no equivalent.

  6. SOCKS inbounds relay UDP by default. Xray’s SOCKS inbound has udp off unless you turn it on; etemenanki-app has it on unless you write udp = false.

  7. "warning" is not a log level. Xray’s default level is "warning". In etemenanki-app the word is read as a log target name, which silences every line, including the error that says why --test failed. Write level = "warn".

The configuration below is a typical Xray server: VLESS over WebSocket and TLS on port 443, a direct exit, and two blocking rules. The tabs show the Xray original and the etemenanki-app translation. The TOML passes --test once the certificate and geodata files exist at the paths given.

config.json
{
"log": { "loglevel": "warning" },
"inbounds": [
{
"tag": "vless-ws-in",
"listen": "0.0.0.0",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{ "id": "11111111-2222-3333-4444-555555555555", "email": "alice@example.com" },
{ "id": "11111111-2222-3333-4444-666666666666", "email": "bob@example.com" }
],
"decryption": "none"
},
"streamSettings": {
"network": "ws",
"security": "tls",
"tlsSettings": {
"certificates": [
{
"certificateFile": "/usr/local/etc/xray/fullchain.pem",
"keyFile": "/usr/local/etc/xray/privkey.pem"
}
]
},
"wsSettings": { "path": "/ray", "host": "proxy.example.com" }
},
"sniffing": {
"enabled": true,
"destOverride": ["http", "tls"],
"routeOnly": true
}
}
],
"outbounds": [
{ "tag": "direct", "protocol": "freedom" },
{ "tag": "block", "protocol": "blackhole" }
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{ "type": "field", "ip": ["geoip:private"], "outboundTag": "block" },
{ "type": "field", "domain": ["geosite:category-ads-all"], "outboundTag": "block" }
]
}
}

What changed, from top to bottom:

  • loglevel became level, and "warning" became "warn".
  • streamSettings became the [inbound.stream] table, with the WebSocket and TLS settings in its ws and tls sub-tables. certificates[0] became the two keys cert_file and key_file.
  • clients became users. The email labels were dropped, because VLESS and VMess users here carry only an id. decryption was dropped, because plain VLESS is the only mode.
  • The sniffing object became one boolean. destOverride has no equivalent: TLS and HTTP are always tried, and nothing else is.
  • The geodata files have explicit paths in [route]. Xray finds geoip.dat and geosite.dat next to its binary; etemenanki-app reads only the paths you give, and only when a rule uses them.
  • The geoip: and geosite: prefixes moved into the key name. "type": "field" and domainStrategy were dropped.
  • [route] default = "direct" is optional here, because the first outbound is the default anyway. Writing it out keeps the file correct if you reorder the outbounds.

Because matchers in one rule are OR, the two blocking rules could also be written as one: geoip = ["private"] and geosite = ["category-ads-all"] in a single [[route.rule]]. The complete walkthrough, with certificates and a test request, is in the VLESS over WebSocket and TLS recipe.

The client side of the same service: a local SOCKS port that sends everything to the server above.

config.json
{
"inbounds": [
{
"tag": "socks-in",
"listen": "127.0.0.1",
"port": 1080,
"protocol": "socks",
"settings": { "auth": "noauth", "udp": true }
}
],
"outbounds": [
{
"tag": "proxy",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "proxy.example.com",
"port": 443,
"users": [{ "id": "11111111-2222-3333-4444-555555555555", "encryption": "none" }]
}
]
},
"streamSettings": {
"network": "ws",
"security": "tls",
"tlsSettings": { "serverName": "proxy.example.com" },
"wsSettings": { "path": "/ray", "host": "proxy.example.com" }
}
}
]
}

The vnext array is gone: server and port sit on the [[outbound]] itself, and the single user’s id moves to [outbound.settings]. An outbound reaches exactly one server. To spread traffic over several, define one outbound per server and put them in a balancer.

Xray etemenanki-app Difference
tag (optional) tag (required) Must be unique among inbounds.
listen, default "0.0.0.0" listen, default "127.0.0.1" A value starting with / is a Unix socket path. Abstract sockets (@name) are refused.
port: a number, a range such as "10000-10010", or a list port: one number Ranges and lists are a type error. Define one inbound per port.
protocol protocol See Protocol names.
settings [inbound.settings] Per protocol; see Users and credentials.
streamSettings [inbound.stream] See Stream settings.
sniffing object sniffing boolean, default true See Sniffing.
allocate None

The full list of inbound keys is on Inbounds.

Xray protocol Inbound Outbound
socks socks (SOCKS4, 4a and 5) socks (SOCKS5)
http http http (CONNECT only)
vless vless vless
vmess vmess vmess
trojan trojan trojan
shadowsocks shadowsocks, TCP only shadowsocks, TCP only
wireguard Refused: wireguard cannot be used as an inbound (no server implementation) wireguard
freedom Not applicable freedom, alias direct
blackhole Not applicable blackhole, alias block
dokodemo-door, tunnel Refused: unknown protocol Not applicable
dns, loopback Not applicable Refused: unknown protocol
hysteria (version 2) hysteria2, aliases hysteria and hy2 Same
tun tun Not applicable

Protocol names are case-sensitive: "Freedom" is refused.

Hysteria 2 is laid out differently from Xray. [inbound.stream] and [outbound.stream] are refused on hysteria2, and its certificate, TLS and authentication keys go in [inbound.settings] or [outbound.settings] instead. See Hysteria 2.

Xray etemenanki-app Difference
tag (optional) tag (required) Must be unique, and must not equal a balancer tag.
protocol protocol
settings.vnext[0].address, .port (VLESS, VMess) server, port On the [[outbound]] table itself. Only one server per outbound.
settings.servers[0].address, .port (Trojan, Shadowsocks, SOCKS, HTTP) server, port Same.
streamSettings [outbound.stream] Same keys as the inbound side.
mux None Refused as an unknown field. The outbound opens one connection per flow.
sendThrough, proxySettings, streamSettings.sockopt None Refused as unknown fields. There is no outbound chaining.
freedom settings.domainStrategy address_family on the outbound See the next table.

address_family is the closest equivalent to Xray’s domainStrategy on freedom. It works on every outbound except blackhole: on freedom it filters and orders the destination’s addresses, and on a proxy outbound it does the same for the server name.

Xray freedom domainStrategy address_family
AsIs, UseIP, ForceIP "auto" (the default)
UseIPv4, ForceIPv4 "ipv4_only"
UseIPv6, ForceIPv6 "ipv6_only"
UseIPv4v6, ForceIPv4v6 "prefer_ipv4"
UseIPv6v4, ForceIPv6v4 "prefer_ipv6"

Names are always resolved by the resolver in [dns], so there is no difference between Xray’s AsIs (system resolver) and UseIP (built-in DNS). The accepted spellings and details are on Outbounds.

Protocol and side Xray etemenanki-app Dropped or refused
VLESS inbound settings.clients[].id users = [{ id = "…" }] flow, email, level, decryption, fallbacks
VLESS outbound vnext[0].users[0].id id flow, encryption
VMess inbound settings.clients[].id users = [{ id = "…" }] alterId, email, level
VMess outbound vnext[0].users[0].id, .security id, security alterId
Trojan inbound settings.clients[].password, .email users = [{ password = "…", email = "…" }] level, flow, fallbacks
Trojan outbound servers[0].password password
Shadowsocks inbound settings.method, .password, .clients[] method, password, users (the name clients is also accepted) network, level
Shadowsocks outbound servers[0].method, .password method, password uot, level
SOCKS inbound auth = "noauth" or "password", accounts[], udp, ip auth = "none" or "password", accounts, udp, udp_bind userLevel
HTTP inbound accounts[], allowTransparent accounts, allow_transparent timeout, userLevel
SOCKS and HTTP outbounds servers[0].users[0].user, .pass user, pass

Some value rules differ from Xray:

  • UUIDs must be real UUIDs. Xray turns an arbitrary short string such as "my-custom-id" into a UUID. etemenanki-app refuses it with invalid uuid "my-custom-id". Give each such user a proper UUID, and update the client to match.
  • VMess is AEAD only. alterId = 0 needs no replacement; remove it. Clients that use alterId greater than 0 cannot connect. On the outbound, security = "auto" always means AES-128-GCM, and "none" and "zero" are refused with unknown vmess security. The inbound accepts only AES-128-GCM and ChaCha20-Poly1305 bodies, so an Xray client set to "none" or "zero" cannot connect.
  • Shadowsocks has no UDP. Both sides carry TCP only, and there are no stream ciphers. The 2022 methods take the same keys as in Xray: the server PSK in password, each user’s PSK in users[].password, and "server-psk:user-psk" in the outbound’s password for a multi-user server.

The protocol pages have the details: VLESS, VMess, Trojan, Shadowsocks, SOCKS and HTTP.

Xray settings [outbound.settings] Difference
secretKey private_key Base64 or hex, 32 bytes.
address, as CIDRs such as "10.0.0.2/32" address, as bare IPs such as "10.0.0.2" A prefix is refused with invalid IP address syntax.
peers[0].publicKey peer_public_key One peer only.
peers[0].preSharedKey preshared_key
peers[0].endpoint endpoint host:port, resolved with the system resolver.
peers[0].keepAlive keepalive Seconds.
peers[0].allowedIPs None Everything routed to the outbound enters the tunnel.
mtu mtu Default 1420.
reserved reserved Exactly three bytes.
domainStrategy address_family on the outbound Applies to destinations inside the tunnel.
noKernelTun None The tunnel always runs in user space.

See WireGuard for the rest.

streamSettings becomes the [inbound.stream] or [outbound.stream] table. The network and security pair translates like this:

Xray network + security [.stream] network + security
tcp or raw, no security Leave [.stream] out, or network = "tcp"
tcp or raw + tls network = "tls", security omitted
ws, no security network = "ws"
ws + tls network = "ws", security = "tls"
grpc, no security network = "grpc"
grpc + tls network = "grpc", security = "tls"
any + reality Not supported: unknown stream security "reality" (expected "tls" or "none")
httpupgrade, splithttp, xhttp, h2, http, kcp, quic Not supported: unknown stream network "…"

The per-transport objects map to sub-tables:

Xray etemenanki-app Notes
wsSettings.path [.stream.ws] path Default /. A missing leading / is added. ?ed=2048 in the path turns on early data on the outbound, as in Xray; the inbound accepts early data from any client.
wsSettings.host, or wsSettings.headers.Host [.stream.ws] host On an inbound, requests with a different Host get 404. On an outbound, it falls back to tls.server_name, then server.
wsSettings.headers (other headers), heartbeatPeriod, acceptProxyProtocol None
grpcSettings.serviceName [.stream.grpc] service_name Required. Only the plain service-name form: the paths are /<service_name>/Tun and /<service_name>/TunMulti.
grpcSettings.authority [.stream.grpc] authority Outbound only. Falls back to tls.server_name, then server.
grpcSettings.multiMode None The inbound accepts both modes. The outbound always uses the default (Tun) mode, so an Xray server works with either setting.
grpcSettings.idle_timeout, health_check_timeout, user_agent, other tuning None Fixed values.
tlsSettings.serverName [.stream.tls] server_name Outbound: SNI and the name the certificate is checked against, default server. Ignored on an inbound.
tlsSettings.allowInsecure [.stream.tls] allow_insecure Outbound only; ignored on an inbound. Cannot be combined with ca_file.
tlsSettings.certificates[0].certificateFile, .keyFile [.stream.tls] cert_file, key_file Inbound only. One certificate and key pair per inbound, as PEM files; the certificate file may hold the full chain. Inline certificate and key arrays are not supported.
A certificate with "usage": "verify" [.stream.tls] ca_file Outbound only; ignored on an inbound. The CA is added to the system roots.
alpn None Fixed: http/1.1 for ws, h2 for grpc, none for tls.
fingerprint, pinnedPeerCertSha256, minVersion, maxVersion, cipherSuites, rejectUnknownSni, ECH None TLS 1.2 or newer on both sides, with fixed cipher settings.
tcpSettings.header (HTTP obfuscation) None
sockopt None

Stream settings apply to http, trojan, vless and vmess inbounds, and to socks, http, trojan, vless, vmess and shadowsocks outbounds. Every key is described on Transports.

Each Xray rule becomes one [[route.rule]] table. The target becomes outbound, whether it names an outbound or a balancer, and each condition becomes one or more matcher keys.

Xray rule field [[route.rule]] key Notes
outboundTag outbound
balancerTag outbound Balancers and outbounds share one set of tags.
domain (and domains) domain_suffix, domain_keyword, domain_full, domain_regex, geosite Split by prefix; see the next table.
ip cidr, geoip Plain IPs and CIDRs go to cidr, geoip: entries to geoip.
port port An array of strings, one port or range each: "53,443,1000-2000" becomes ["53", "443", "1000-2000"].
network network One value, "tcp" or "udp". For "tcp,udp", leave the key out.
source, sourceIP source_cidr The client’s address.
inboundTag inbound_tag
type: "field", ruleTag None Leave them out.
sourcePort, localIP, localPort, user, protocol, attrs, process, vlessRoute, webhook None Unknown field.

Domain entries split by their prefix:

Xray domain entry etemenanki-app Matches
"domain:example.com" domain_suffix = ["example.com"] example.com and every subdomain, not notexample.com
"full:example.com" domain_full = ["example.com"] Exactly example.com
"keyword:example" domain_keyword = ["example"] Any domain that contains the text
"example" (no prefix) domain_keyword = ["example"] Same: Xray treats a bare string as a keyword
"regexp:\\.example\\.com$" domain_regex = ['\.example\.com$'] Rust regex syntax, which, like Go’s, has no look-around
"geosite:cn" geosite = ["cn"] Code names are case-insensitive
"geosite:google@ads" geosite = ["google@ads"] Only entries with that attribute
"ext:file.dat:tag" None One geosite file per configuration
"dotless:" domain_regex = ['^[^.]*$'] Write the regex yourself

And IP entries:

Xray ip entry etemenanki-app
"10.0.0.0/8", "192.0.2.1" cidr = ["10.0.0.0/8", "192.0.2.1"]
"geoip:cn" geoip = ["cn"]
"geoip:!cn" geoip = ["!cn"]
"geoip:private" geoip = ["private"], which must exist in your geoip.dat
"ext:file.dat:tag" None

The domain matchers compare in lower case: domain_suffix, domain_keyword and domain_full values are lower-cased, and so is every domain they are matched against. domain_regex patterns are not, so write them in lower case. A cidr must have its host bits zero: "10.0.0.1/8" fails with invalid cidr "10.0.0.1/8": host part of address was not zero, where Xray would accept it.

Geodata files are the same v2ray-format geoip.dat and geosite.dat that Xray uses. Set their paths in [route] as geoip and geosite. A file is read only when some rule uses it, and a rule that uses one without a path fails with a geosite matcher is used but no geosite file is configured.

In Xray, a rule with several fields matches only if all of them match. In etemenanki-app, a rule matches as soon as any one of its matchers matches, whichever key it is under. Rules are still tried in order and the first match wins, as in Xray.

This Xray rule blocks QUIC, and nothing else:

{ "type": "field", "network": "udp", "port": "443", "outboundTag": "block" }

Translated key by key, it blocks all UDP and all TCP to port 443:

[[route.rule]]
outbound = "block"
network = "udp" # matches every UDP flow...
port = ["443"] # ...and, separately, every flow to port 443

A combined condition like this cannot be expressed. Before converting a rule that has more than one field, decide whether each field works on its own, and split the rule or drop fields until it does. A rule with a single field, or with several values in one field, translates unchanged.

etemenanki-app’s router never resolves a name. It behaves like Xray’s "domainStrategy": "AsIs", and there is no IPIfNonMatch or IPOnDemand:

  • A cidr or geoip matcher never matches a flow addressed by domain name.
  • A domain matcher, including geosite, never matches a flow addressed by IP, unless sniffing recovered a name for it.
flowchart TB
    F["Flow arrives"] --> D{"Destination is an IP?"}
    D -- "no, a domain" --> R["Rules in order: domain matchers see the domain"]
    D -- "yes" --> S{"TCP, and sniffing = true?"}
    S -- "yes" --> N["Read TLS SNI or HTTP Host"]
    S -- "no" --> I["Rules in order: IP matchers see the address"]
    N --> I2["Rules in order: IP matchers see the address, domain matchers see the sniffed name"]
    R --> O["First rule with any matching matcher wins"]
    I --> O
    I2 --> O
    O -- "none matched" --> DEF["route.default"]

If your Xray setup used IPIfNonMatch to send, for example, domestic sites direct by their IP (geoip:cn), add the domain side of the same rule as well (geosite = ["cn"]), so that flows addressed by name match too.

Xray sniffing etemenanki-app
enabled sniffing = true or false on the inbound. The default is true.
destOverride None. TLS SNI and HTTP Host are always tried; QUIC, fakedns and BitTorrent are not sniffed.
routeOnly Always on in effect: the sniffed name is used for routing and the flow still goes to the original address.
metadataOnly, domainsExcluded, ipsExcluded None

Only TCP flows whose destination is an IP address are sniffed, for up to 300 ms and 4 KiB of the first payload. Flows addressed by name route on that name without waiting. See Inbounds and Routing.

Xray balancers live inside routing and depend on observatory for health. In etemenanki-app, [[balancer]] is a top-level table that probes its own members.

{
"routing": {
"balancers": [
{
"tag": "auto",
"selector": ["proxy-"],
"strategy": { "type": "leastPing" },
"fallbackTag": "direct"
}
],
"rules": [{ "type": "field", "network": "tcp,udp", "balancerTag": "auto" }]
},
"observatory": {
"subjectSelector": ["proxy-"],
"probeUrl": "https://www.gstatic.com/generate_204",
"probeInterval": "1m"
}
}
Xray etemenanki-app
selector (tag prefixes) outbounds, a list of exact outbound tags. Other balancers cannot be members.
strategy.type = "random" (the default), "roundRobin", "leastPing", "leastLoad" strategy = "failover" (the default: the first healthy member in list order) or "round_robin". Anything else fails with unknown balancer strategy.
fallbackTag None. When every member is down, the first member is used.
observatory.probeUrl, probeInterval A TCP connect to each member’s server and port every probe_interval seconds (default 30), with probe_timeout (default 5).

Because the probe is a TCP connect to server and port, only outbounds with a TCP upstream can be members. freedom, blackhole and wireguard have no server, and hysteria2 listens on UDP, so all four are refused with balancer auto: outbound direct has no upstream a TCP health probe can reach, so it cannot be balanced. See Balancers and the failover recipe.

Xray’s dns object lists several servers and can pick among them by domain. etemenanki-app’s [dns] configures one upstream, and almost every name the proxy resolves itself goes there: freedom destinations, proxy outbound server names, destinations inside a WireGuard tunnel and balancer probes. The exception is the WireGuard endpoint, which always goes to the host resolver. With the default backend = "system", [dns] is the host resolver too, which honours /etc/hosts.

Xray dns.servers entry [dns]
"localhost" backend = "system", the default
"1.1.1.1" backend = "udp", server = "1.1.1.1:53"
"https://cloudflare-dns.com/dns-query" backend = "https", server = "1.1.1.1:443", url = "https://cloudflare-dns.com/dns-query"
"tcp://…", "quic+local://…", "fakedns" None
— backend = "tls", server = "1.1.1.1:853", server_name = "cloudflare-dns.com" (DNS over TLS)

server is always an IP address with a port: a bare "1.1.1.1" fails with dns: invalid server address: invalid socket address syntax. hosts, per-server domains and expectIPs, queryStrategy, clientIp and disableCache have no equivalent. Answers are always cached. See DNS.

These Xray features have no equivalent. Most of them fail loudly, so --test finds them for you. The rows marked “accepted and ignored” do not, so search your configuration for them.

Xray feature What happens in etemenanki-app
XTLS flow, including xtls-rprx-vision Unknown field on both sides. A client that sends a flow anyway is disconnected.
REALITY unknown stream security "reality". Use a certificate and tls.
httpupgrade, splithttp, xhttp transports unknown stream network. Also h2, http, kcp and quic.
Client-side mux on an outbound Unknown field. VLESS, VMess and Trojan inbounds accept mux.cool (and XUDP inside it) from Xray clients without any setting.
fallbacks on VLESS and Trojan Unknown field. Connections that fail authentication are closed.
Hysteria 2 Brutal congestion control (up and down bandwidth) No bandwidth keys. A connection negotiates as “rate unknown” and congestion control decides the rate.
freedom domainStrategy or targetStrategy, redirect, fragment, noises Accepted and ignored: freedom never reads its settings. Use address_family for the strategy.
blackhole response Accepted and ignored. A blocked flow gets no data and no HTTP 403 page.
Balancers driven by observatory or burstObservatory Unknown top-level field. Each balancer runs its own TCP probe.
leastPing, leastLoad and random balancer strategies unknown balancer strategy. Xray’s roundRobin is spelled round_robin.
stats, api, metrics, per-user traffic counters Unknown top-level field. katana adds per-user traffic accounting for panel nodes.
policy levels and level on users Unknown field. Timeouts are fixed.
dokodemo-door inbound, dns and loopback outbounds unknown protocol.
WireGuard inbound Refused: no server implementation.
fakedns, reverse, outbound chaining (proxySettings, dialerProxy) Unknown field.
uTLS fingerprint, certificate pinning, ECH Unknown field.
Inbound port ranges, allocate Type error or unknown field.
email on VLESS and VMess users Unknown field. Trojan, Shadowsocks and Hysteria 2 users do take an email.
Shadowsocks over UDP Not supported on either side.

The project’s integration tests run etemenanki-app against a real xray-core binary, pass traffic through both, and compare the bytes that come back.

Combination etemenanki-app as client, Xray as server Xray as client, etemenanki-app as server
VLESS over TLS (network = "tls", Xray tcp + tls) Tested Tested
VLESS over WebSocket, plain and with TLS Tested Tested
VLESS over gRPC, plain and with TLS Tested Tested
VMess over gRPC with TLS Not tested Tested
VMess over plain WebSocket with early data (path = "/vmess?ed=2048") Tested Tested
mux.cool from an Xray client: VLESS over TCP and over WebSocket with TLS, Trojan over WebSocket with TLS Not applicable Tested
mux.cool from an Xray client: VMess over TCP, including a 64 KiB upload that is split across both VMess chunks and 8 KiB mux frames Not applicable Tested
XUDP (UDP inside mux) over VLESS and VMess Not applicable Tested
XUDP over VLESS with one UDP association talking to two peers, each reply attributed to the peer that sent it Not applicable Tested
Sniffing an IP-addressed VLESS flow from an Xray client and routing it by the HTTP Host or the TLS SNI Not applicable Tested

Trojan without mux, Shadowsocks, SOCKS, HTTP and WireGuard have no test against an Xray binary. Hysteria 2 is tested against the upstream Hysteria client and server in both directions; see Hysteria 2.

  1. List the Xray features your configuration uses and compare them with Not supported. If you rely on REALITY, XTLS Vision, xhttp or client-side mux, stay on Xray for that part, or change the clients first.

  2. Convert the file one section at a time with the tables above: inbounds, outbounds, stream settings, then routing. Keep the tags the same so the rules still refer to the right outbounds.

  3. Go through every rule that has more than one condition and rewrite it for OR semantics.

  4. Copy geoip.dat and geosite.dat to a fixed location and set their paths in [route]. Use absolute paths: relative ones are resolved against the working directory, not the configuration file.

  5. Check the file, and repeat until it passes. Only the first error is reported each time.

    Terminal window
    etemenanki-app --test -c /etc/etemenanki/config.toml
  6. Search the file for listen, [outbound.settings] under freedom or blackhole, and [log], and compare them with Differences that change behaviour. These are the mistakes --test cannot catch.

  7. Start etemenanki-app on a spare port, point one client at it, and check that traffic flows and each rule sends it where you expect. Then move the listener to the production port.

These are the errors that Xray habits produce most often. Build errors are prefixed with inbound <tag>: or outbound <tag>: where they concern one entry.

Error Xray habit Fix
security = "tls" is not valid with network = "tcp"; … "network": "tcp", "security": "tls" network = "tls"
unknown stream security "reality" (expected "tls" or "none") REALITY Use security = "tls" with a certificate
unknown stream network "xhttp" An unsupported transport Use ws or grpc
unknown field `mux`, expected one of `tag`, `protocol`, `server`, `port`, `stream`, `address_family`, `settings` mux on an outbound Remove it
invalid settings: unknown field `clients`, expected `users` clients on a VLESS, VMess or Trojan inbound Rename it to users
invalid settings: unknown field `flow`, expected `id` XTLS Vision, or email and level, on a user Remove the key
invalid settings: unknown field `alterId`, expected `id` VMess alterId on an inbound user Remove it
outbound <tag>: missing server vnext or servers inside settings Move the address to server and port on the outbound
invalid settings: unknown field `vnext`, expected `id` Same, with server already set Remove vnext and put the user’s id directly in [outbound.settings]
unknown socks auth "noauth" Xray’s name for no authentication auth = "none"
invalid type: map, expected a boolean at sniffing The Xray sniffing object sniffing = true
invalid type: string "10000-10010", expected u16 A port range on an inbound One inbound per port
unknown field `outboundTag` outboundTag or balancerTag in a rule outbound = "…"
unknown field `domain` domain or ip in a rule Split into the matcher keys listed above
invalid rule network "tcp,udp" (expected "tcp" or "udp") Both networks in one string Leave network out
invalid port spec: "53,443" A comma-separated port list port = ["53", "443"]
invalid type: integer `443`, expected a string A number in a rule’s port port = ["443"]
geosite code not found: geosite:cn The prefix kept in the value geosite = ["cn"]
unknown balancer strategy "leastPing" (expected "failover" or "round_robin") An Xray strategy, including "roundRobin" "failover" or "round_robin"
unknown field `selector` Balancer tag prefixes List exact tags in outbounds
unknown field `servers`, expected one of `backend`, `server`, `server_name`, `url`, `ca_file` Xray’s DNS server list One resolver; see DNS
unknown field `stats` (or api, policy, observatory) Xray top-level sections Remove them
--test prints nothing and exits with status 1 level = "warning" level = "warn". Run RUST_LOG=info etemenanki-app --test -c <file> to see the hidden error.