Skip to content

Command line

This page is the reference for the two binaries: etemenanki-app, the standalone proxy, and katana, the panel node agent. It lists every flag and its default, what the --test dry run checks and what it cannot check, the environment variables that change their behaviour, their exit codes and signal handling, and the exact text of every message the process prints outside the protocol code.

It describes the etemenanki 2.0.2 release, whose etemenanki-app still reports version 2.0.0 and is built with etemenanki-protocols 2.0.2, and katana 3.0.1, which is built with etemenanki-protocols 2.0.1. For a walk-through of running either program as a service, with systemd units and an upgrade procedure, see Running in production.

Terminal window
etemenanki-app [-c <path>] [--test]
etemenanki-app -h | --help
etemenanki-app -V | --version
katana [-c <path>] [--test]
katana -h | --help
katana -V | --version

Both programs run in the foreground until they receive SIGINT or SIGTERM. Neither has subcommands, positional arguments, a daemon mode or a PID file.

Both binaries accept the same four options and nothing else.

Option Argument Default What it does
-c, --config path config.toml The TOML configuration file to load. A relative path is resolved against the working directory of the process.
--test none off Build the whole configuration, print the result and exit, without binding a socket or contacting any server. See Checking a configuration.
-h, --help none Print the help text to standard output and exit with status 0.
-V, --version none Print the program name and version, for example etemenanki-app 2.0.0 or katana 3.0.1, and exit with status 0.

The path can be written in any of the forms the argument parser accepts: -c /etc/etemenanki/config.toml, -c/etc/etemenanki/config.toml, --config /etc/etemenanki/config.toml or --config=/etc/etemenanki/config.toml. The options can come in any order.

A usage mistake prints an error, the usage line and a hint to standard error, and exits with status 2. Nothing is loaded.

Mistake Message
Unknown option or stray argument error: unexpected argument '--bogus' found
-c without a path error: a value is required for '--config <CONFIG>' but none was supplied
An option given twice error: the argument '--test' cannot be used multiple times

The help text is short. For reference, this is what each program prints:

$ etemenanki-app --help
A minimal Xray-core-style proxy runtime
Usage: etemenanki-app [OPTIONS]
Options:
-c, --config <CONFIG> Path to the TOML configuration file [default: config.toml]
--test Validate the configuration and exit without binding any listener
-h, --help Print help
-V, --version Print version

The default, config.toml, means “a file called config.toml in the current working directory”. Under a service manager the working directory is often /, so pass -c with an absolute path.

Every relative path inside the file is also resolved against the working directory, not against the directory that holds the config file. That covers cert_file, key_file, ca_file, geoip, geosite, rule_list_path and Unix socket listen paths.

When the file cannot be opened, the error is the bare operating-system message, for example No such file or directory (os error 2), Permission denied (os error 13) or Is a directory (os error 21). It does not repeat the path, and the same message appears when a certificate, CA bundle or geodata file named inside the config is missing. Check -c first, then every path in the file.

While it runs, each program watches the directory that contains the config file, and reloads when the file changes. With a bare file name such as the default, that directory is the working directory. See etemenanki-app hot reload and katana hot reload.

For etemenanki-app, --test runs the same build a start runs and stops just before the program would open sockets. For katana, it runs the part of a start that needs no panel, plus a check of each Hysteria 2 node’s local settings. Both read every file the build needs, so run it as the user the service runs as, from the service’s working directory: an unreadable key or a wrong relative path then fails the check instead of the start.

The check stops at the first error. Fix it and run the check again to find the next one.

flowchart LR
  P["read and parse the file"] --> A{"any outbound?"}
  A -- "no" --> F["error"]
  A -- "yes" --> D["DNS resolver"]
  D --> O["outbounds"]
  O --> B["balancers"]
  B --> R["routing table"]
  R --> I["inbounds"]
  I --> OK["Configuration OK."]

In order, etemenanki-app --test:

Step What is checked Files read
Parse The file is UTF-8 and valid TOML, every key is known, every value has the right type. The keys inside an inbound’s or outbound’s settings table are checked later, when that inbound or outbound builds. The config file
Outbounds present The file has at least one [[outbound]]. The first one is the default route unless [route].default names another.
DNS [dns] names a known backend with the settings that backend needs. [dns].ca_file, when set
Outbounds Each [[outbound]] builds: protocol, settings, keys, stream settings, TLS options. Tags are unique. Each outbound’s TLS ca_file, and a Hysteria 2 outbound’s ca_file
Balancers Each balancer’s tag is new, every member names an outbound, every member has an upstream a health probe can reach, and the strategy is known.
Routing [route].default and every rule name an existing outbound or balancer, and every matcher is valid. The geoip file only when a rule uses a geoip matcher, and the geosite file only when a rule uses a geosite matcher
Inbounds Each [[inbound]] builds: protocol, users, listen address, stream settings. Tags are unique. TLS and Hysteria 2 certificates and private keys

--test does not check:

  • that a port is free, or that the process may bind it;
  • that the process may create a TUN device or install its routes;
  • that upstream servers or DNS servers are reachable, or that their certificates verify;
  • that the system trust store can be loaded (the TLS client and the Hysteria 2 client read it when they connect);
  • that a Hysteria 2 outbound’s ca_file holds a usable certificate: --test reads the file, but its contents are parsed only when the outbound connects;
  • [log].level, which is never validated (see Logging).

A normal start runs the same build and then binds the listeners, so a start fails on everything --test fails on, plus bind errors and TUN device errors.

The two programs report the result differently. etemenanki-app writes its error through the logger, and katana writes it directly to standard error.

etemenanki-app katana
Success Configuration OK. on standard output, exit 0 Configuration OK on standard output (no full stop), exit 0
Failure configuration invalid: <error> as an ERROR log line on standard output, exit 1 configuration error: <error> on standard error, exit 1
Parse error prefix none config parse error:
Invalid UTF-8 invalid utf-8 sequence of 1 bytes from index 0 config not utf-8: invalid utf-8 sequence of 1 bytes from index 0
$ etemenanki-app --test -c config.toml
Configuration OK.
$ etemenanki-app --test -c config.toml
2026-09-24T20:33:57.764733Z ERROR etemenanki_app: configuration invalid: TOML parse error at line 6, column 1
|
6 | bogus=1
| ^^^^^
unknown field `bogus`, expected one of `tag`, `protocol`, `listen`, `port`, `stream`, `address_family`, `sniffing`, `settings`

Other failures you are likely to meet:

Message after configuration invalid: Cause
No such file or directory (os error 2) The config file, or a file it names, does not exist at that path.
config defines no outbounds The file has no [[outbound]]. An empty file fails this way.
duplicate outbound tag: <tag> Two [[outbound]] sections share a tag.
duplicate inbound tag: <tag> Two [[inbound]] sections share a tag.
outbound <tag>: unknown protocol "<name>" protocol names no outbound protocol.
balancer <tag> references unknown outbound tag: <member> A balancer member is not an outbound tag.

Errors lists the messages of every section.

Check a new file before it replaces the live one. The exit status is the reliable signal: 0 means the configuration builds, anything else means it does not.

check-and-install.sh
#!/bin/sh
set -eu
new=/etc/etemenanki/config.toml.new
live=/etc/etemenanki/config.toml
if ! out=$(cd /etc/etemenanki && etemenanki-app --test -c "$new" 2>&1); then
printf 'config rejected:\n%s\n' "$out" >&2
exit 1
fi
mv "$new" "$live" # the running process notices the change and reloads

For katana, replace the paths and the binary name. Capture both streams (2>&1) so the script sees katana’s standard-error message and etemenanki-app’s log line alike.

Status etemenanki-app katana
0 --test passed; --help or --version printed; or a graceful stop after SIGINT or SIGTERM. Same.
1 --test failed; or the start failed because the file could not be read or parsed, did not build, an inbound could not bind, or a TUN inbound could not create its device. --test failed; or the start failed because the file could not be read or parsed, the outbound pool did not build, the file has no [[node]], or not a single node’s panel client and routing table could be built.
2 Command-line usage error. Same.
101 A panic, for example an invalid TOKIO_WORKER_THREADS. Same.
128 + n in a shell Killed by signal n, for example 129 for SIGHUP, 137 for SIGKILL. Same.

Once a process is running, only a signal ends it. Failures after that point are logged, and the process carries on:

  • A reload whose file cannot be read, parsed or built keeps the old configuration, and the process keeps running. In etemenanki-app, an inbound that cannot bind during a reload is left out and the other inbounds start.
  • A katana node whose panel stops answering after it has started logs a warning and tries again at the next poll.
  • A katana node that fails its first start, for example because the panel could not be reached or its port could not be bound, retries until it comes up. Each failed attempt logs node <id>: <reason>; retrying in <N>s, where <reason> is node_info failed: …, panel returned no node info, panel returned port 0, user_list failed: …, panel returned no user list or initial start failed: …. The first wait is 1 second, and it doubles after each failure up to 60 seconds, or up to the node’s update_periodic when that is shorter. An edit to the node’s [[node]] section makes it try again at once. katana does not exit while a node retries, even when that is its only node, so a service manager sees a running process. Nodes lists these errors.

Neither program has a reload signal. A reload happens when the config file changes on disk.

The handlers for SIGINT and SIGTERM are installed only once startup has finished: in etemenanki-app after every listener is bound, in katana after the nodes have been spawned. A signal that arrives earlier, or during --test, terminates the process at once with the kernel’s default action.

Signal etemenanki-app katana
SIGINT (Ctrl-C) Graceful stop, exit 0 Graceful stop, exit 0
SIGTERM Graceful stop, exit 0 Graceful stop, exit 0
SIGHUP Not handled: the process is terminated at once, without a shutting down line Same
SIGKILL Terminated at once Terminated at once; traffic not yet reported to the panel is lost

A graceful stop logs shutting down, then:

  • etemenanki-app cancels the running configuration: it closes every listener and every open connection at once, without waiting for connections to finish, and exits.
  • katana cancels every node. Each running node closes its listeners and connections, so its traffic counters are final, and then flushes them to the panel: one last traffic report if any user has unreported traffic and disable_upload_traffic is off, and one audit report if any audit rule matched. Each request can take up to the node’s api.timeout, which is 5 seconds when unset or 0. If that last report fails, katana logs node <id>: report traffic: <error> and the traffic is lost, because the counters live only in memory. The process exits when every node has finished.

After the first SIGINT or SIGTERM, the program keeps its handlers, so further SIGINT and SIGTERM signals do not interrupt the stop. Only SIGKILL ends a stop that hangs.

Neither program reads an environment variable of its own. The variables below are read by the libraries they are built on.

Variable etemenanki-app katana Effect
RUST_LOG yes yes The log filter. Replaces [log].level at start when it parses. See Logging.
NO_COLOR yes yes Any non-empty value, even 0, removes the ANSI colour codes from log lines.
TOKIO_WORKER_THREADS yes yes The number of worker threads of the async runtime. The default is the number of CPUs. A value that is not a positive integer makes the process panic at once and exit with status 101, even with --version.
SSL_CERT_FILE, SSL_CERT_DIR yes yes Where TLS clients look for trusted root certificates. See Trusted root certificates.
HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, NO_PROXY (or lower case) no yes katana sends its panel requests through the proxy these name, the way curl does. A proxy variable left in a service’s environment therefore also carries the panel traffic. Proxy traffic from users is not affected.

Each TLS client finds the system’s trusted roots in its own way, and SSL_CERT_FILE and SSL_CERT_DIR change each one differently. In all cases, these variables replace a default location rather than add to it. To trust one extra CA for one connection, prefer the ca_file key of that outbound or of [dns]: it adds the CA to the system roots.

Client Program Roots without the variables With SSL_CERT_FILE or SSL_CERT_DIR set
Outbound TLS ([outbound.stream] with security = "tls") and the [dns] tls and https backends etemenanki-app The system OpenSSL library’s default file and directory. On Debian and Ubuntu, these hold the system bundle. OpenSSL reads the file named by SSL_CERT_FILE instead of its default file, and the directory named by SSL_CERT_DIR instead of its default directory.
Hysteria 2 outbound etemenanki-app The system bundle, found by probing the usual locations such as /etc/ssl/certs. Only the file and directories the variables name are read; the system locations are skipped. SSL_CERT_DIR may list several directories separated by :. If no certificate can be loaded, connections fail with hysteria2: no system root certificates could be loaded, even when the outbound sets ca_file.
Panel client (api.host over HTTPS) katana The system bundle, found by probing the usual locations. A variable that names an existing file or directory is used in place of the probed file, and alongside the probed directories.
The [dns] tls and https backends katana OpenSSL’s built-in default under /usr/local/ssl As for etemenanki-app: the variables replace OpenSSL’s defaults.

Both programs log to standard output, one line per event, with a UTC timestamp, the level, the module that logged the line and the message. The lines contain ANSI colour codes even when standard output is not a terminal; set NO_COLOR=1 to remove them.

2026-09-24T20:35:11.971456Z INFO etemenanki_app::instance: inbound socks-in listening on 127.0.0.1:1080
2026-09-24T20:35:13.967835Z INFO etemenanki_app: shutting down

The filter comes from the first of these that applies:

  1. RUST_LOG, if it is set and parses. A RUST_LOG that does not parse is ignored as a whole. A RUST_LOG that is set but empty parses as “no directives” and silences every line.
  2. [log].level from the config file. If the file cannot be read or parsed, this step is skipped, so the parse error is still printed.
  3. info.

Both the variable and the key take the same directive syntax: a comma-separated list where a bare level sets the default and target=level overrides it for one module, for example info,etemenanki_protocols=debug. The targets are the crate names with _ for -: etemenanki_app, katana, etemenanki_protocols, etemenanki_environment and etemenanki_concepts. A single directive in [log].level that cannot be parsed, such as =[, is dropped at start with ignoring `=[`: invalid filter directive on standard error, and the rest applies. If nothing is left, or [log].level is an empty string, only ERROR lines are logged.

katana also applies a changed [log].level on reload. That replaces the filter from RUST_LOG, and a value that does not parse is refused with invalid log level "<value>": <error>. etemenanki-app reads [log].level only at start.

KeyTypeRequiredDefaultDescription
levelstringno"info"A tracing EnvFilter directive, the same syntax as RUST_LOG: a level (off, error, warn, info, debug, trace, or a number from 0 to 5, case-insensitive), optionally followed by per-target overrides such as "warn,etemenanki_protocols=debug". A RUST_LOG variable that parses as a filter replaces this value, even an empty one, which enables nothing. The value is not validated: a word that is not a level, such as "warning", is read as a target name and silences every log line, errors included. An empty value, or one in which no directive parses, logs errors only. Read once at startup and not applied on reload.

Running in production has a table of useful filters and explains how each program handles a changed [log].level at run time.

These are the lines the process itself prints around startup, reload and shutdown. Messages from the protocols, the panel client and the nodes are described on the pages for those features and on Errors. In the tables, <error> stands for the underlying error text and <tag> for an inbound or outbound tag.

Level Message Meaning
INFO inbound <tag> listening on <bind> An inbound is up. <bind> is <host>:<port> for TCP, udp <host>:<port> for Hysteria 2, unix:<path> for a Unix socket and tun <name> (or tun auto) for TUN.
INFO inbound <tag> owns tun device <name> A TUN inbound created its device. Printed before the listening on line.
ERROR failed to start: <error> The start failed. The process exits with 1. A bind error reads failed to start: inbound <tag> bind <bind> failed: <error>.
ERROR config hot-reload disabled: <error> The config directory could not be watched. The proxy keeps running, but edits to the file are not applied until a restart.
INFO config reload: <changes> A reload is being applied; <changes> summarises what differs.
ERROR reload: cannot read <path>: <error> The file could not be read on reload. The current configuration stays.
ERROR reload: parse failed, keeping current config: <error> The new file does not parse. The current configuration stays.
ERROR reload: build failed, keeping current config: <error> The new file parses but does not build. The current configuration stays.
ERROR inbound <tag> bind <bind> failed: <error> On reload, one inbound could not bind. The other inbounds start without it.
INFO shutting down SIGINT or SIGTERM was received.

etemenanki-app hot reload explains the reload lines in detail.

Symptom Cause Fix
No such file or directory (os error 2) although the config exists A relative path inside the file is resolved against the working directory, or a geodata, certificate or CA file is missing. Run from the directory the paths are relative to, or use absolute paths.
--test exits with 1 and prints nothing (etemenanki-app) RUST_LOG or [log].level hides ERROR lines. Run env -u RUST_LOG etemenanki-app --test -c … and fix [log].level.
The service stops when you reload it with SIGHUP Neither program handles SIGHUP. Save the config file instead; it reloads by itself.
katana passes --test, then a node never comes up --test does not contact the panel. The node’s first panel requests or its first start keep failing, and the node keeps retrying. Read the node’s retrying in lines in the log and fix the cause. The node comes up at its next attempt, without a restart; see Troubleshooting.
Panel requests go to an unexpected host (katana) A HTTPS_PROXY, HTTP_PROXY or ALL_PROXY variable is set in katana’s environment. Remove it, or add the panel host to NO_PROXY.