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.
At a glance
Section titled “At a glance”| 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.
Differences that change behaviour
Section titled “Differences that change behaviour”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.
-
Inbounds listen on loopback by default. Xray binds
0.0.0.0whenlistenis missing. etemenanki-app binds127.0.0.1, so a server that must be reachable from outside needslisten = "0.0.0.0"(or a specific address) written out. -
TLS over TCP is
network = "tls". Xray’s"network": "tcp"with"security": "tls"is refused, not translated.security = "tls"is only for adding TLS underwsorgrpc. -
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"andport = ["443"]catches all UDP and all port 443 traffic, not just QUIC. See Rules are OR, not AND. -
Sniffing is on by default and only affects routing. Xray sniffs only when
sniffing.enabledis true and, unlessrouteOnlyis set, rewrites the destination to the sniffed name. etemenanki-app sniffs every IP-addressed TCP flow unless you setsniffing = false, and uses the name for routing only, which is what Xray does with"routeOnly": true. -
freedomandblackholeignore theirsettings. These two outbounds never read[outbound.settings], so Xray’sdomainStrategy,redirect,fragmentandresponsepass--testthere and do nothing. Useaddress_familyon the outbound instead ofdomainStrategy; the others have no equivalent. -
SOCKS inbounds relay UDP by default. Xray’s SOCKS inbound has
udpoff unless you turn it on; etemenanki-app has it on unless you writeudp = false. -
"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--testfailed. Writelevel = "warn".
A converted server
Section titled “A converted server”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.
{ "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" } ] }}[log]level = "warn" # Xray's "warning" is spelled "warn"
[[inbound]]tag = "vless-ws-in"protocol = "vless"listen = "0.0.0.0" # the default is 127.0.0.1port = 443sniffing = true # the default; routing only, like "routeOnly": true
[inbound.stream]network = "ws"security = "tls"
[inbound.stream.ws]path = "/ray"host = "proxy.example.com"
[inbound.stream.tls]cert_file = "/etc/etemenanki/fullchain.pem"key_file = "/etc/etemenanki/privkey.pem"
[inbound.settings]users = [ { id = "11111111-2222-3333-4444-555555555555" }, { id = "11111111-2222-3333-4444-666666666666" },]
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "block"protocol = "blackhole"
[route]default = "direct"geoip = "/etc/etemenanki/geoip.dat"geosite = "/etc/etemenanki/geosite.dat"
[[route.rule]]outbound = "block"geoip = ["private"]
[[route.rule]]outbound = "block"geosite = ["category-ads-all"]What changed, from top to bottom:
loglevelbecamelevel, and"warning"became"warn".streamSettingsbecame the[inbound.stream]table, with the WebSocket and TLS settings in itswsandtlssub-tables.certificates[0]became the two keyscert_fileandkey_file.clientsbecameusers. Theemaillabels were dropped, because VLESS and VMess users here carry only anid.decryptionwas dropped, because plain VLESS is the only mode.- The
sniffingobject became one boolean.destOverridehas no equivalent: TLS and HTTP are always tried, and nothing else is. - The geodata files have explicit paths in
[route]. Xray findsgeoip.datandgeosite.datnext to its binary; etemenanki-app reads only the paths you give, and only when a rule uses them. - The
geoip:andgeosite:prefixes moved into the key name."type": "field"anddomainStrategywere 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.
A converted client
Section titled “A converted client”The client side of the same service: a local SOCKS port that sends everything to the server above.
{ "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" } } } ]}[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[inbound.settings]auth = "none" # Xray's "noauth"udp = true # the default here
[[outbound]]tag = "proxy"protocol = "vless"server = "proxy.example.com" # vnext[0].addressport = 443 # vnext[0].port
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ray"host = "proxy.example.com" # optional: falls back to server_name, then server
[outbound.stream.tls]server_name = "proxy.example.com" # optional: falls back to server
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"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.
Inbounds and outbounds
Section titled “Inbounds and outbounds”Common inbound fields
Section titled “Common inbound fields”| 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.
Protocol names
Section titled “Protocol names”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.
Common outbound fields
Section titled “Common outbound fields”| 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.
Users and credentials
Section titled “Users and credentials”| 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 withinvalid uuid "my-custom-id". Give each such user a proper UUID, and update the client to match. - VMess is AEAD only.
alterId = 0needs no replacement; remove it. Clients that usealterIdgreater than 0 cannot connect. On the outbound,security = "auto"always means AES-128-GCM, and"none"and"zero"are refused withunknown 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 inusers[].password, and"server-psk:user-psk"in the outbound’spasswordfor a multi-user server.
The protocol pages have the details: VLESS, VMess, Trojan, Shadowsocks, SOCKS and HTTP.
WireGuard outbound
Section titled “WireGuard outbound”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.
Stream settings
Section titled “Stream settings”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.
Routing
Section titled “Routing”Rule fields
Section titled “Rule fields”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.
Rules are OR, not AND
Section titled “Rules are OR, not AND”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 443A 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.
No domainStrategy
Section titled “No domainStrategy”etemenanki-app’s router never resolves a name. It behaves like Xray’s "domainStrategy": "AsIs", and there is no IPIfNonMatch or IPOnDemand:
- A
cidrorgeoipmatcher 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.
Sniffing
Section titled “Sniffing”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.
Balancers
Section titled “Balancers”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" }}[[balancer]]tag = "auto"outbounds = ["proxy-a", "proxy-b"] # exact tags, in priority orderstrategy = "failover" # or "round_robin"probe_interval = 60 # secondsprobe_timeout = 5 # seconds
[route]default = "auto"| 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.
Not supported
Section titled “Not supported”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. |
Tested interoperability with Xray
Section titled “Tested interoperability with Xray”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.
Migration checklist
Section titled “Migration checklist”-
List the Xray features your configuration uses and compare them with Not supported. If you rely on REALITY, XTLS Vision,
xhttpor client-side mux, stay on Xray for that part, or change the clients first. -
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.
-
Go through every rule that has more than one condition and rewrite it for OR semantics.
-
Copy
geoip.datandgeosite.datto a fixed location and set their paths in[route]. Use absolute paths: relative ones are resolved against the working directory, not the configuration file. -
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 -
Search the file for
listen,[outbound.settings]underfreedomorblackhole, and[log], and compare them with Differences that change behaviour. These are the mistakes--testcannot catch. -
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.
Common errors
Section titled “Common errors”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. |