The configuration file
katana reads one TOML file that describes everything the panel does not: which panels to talk to, which certificates to serve, where traffic may go, and how names are resolved. This page covers the file as a whole: its four top-level sections, the rules katana applies when it parses them, the process-wide [log] and [dns] sections, and how several [[node]] blocks share one process.
Read it when you write your first config, when you add a second node, or when --test rejects a file and you want to know why. The contents of each node, the outbounds and the routing rules each have their own page, linked from the tables below.
A minimal file
Section titled “A minimal file”The smallest useful file names one panel node. Everything else has a default:
[[node]]panel_type = "NewV2board"
[node.api]host = "https://panel.example.com"node_id = 1key = "replace-with-the-panel-key"node_type = "V2ray"Check it without starting anything:
katana --test -c /etc/katana/config.tomlA valid file prints Configuration OK and exits with status 0. An invalid one prints configuration error: followed by the reason, and exits with status 1. Without -c, katana reads config.toml from the working directory.
This file is enough for --test, but a real node usually needs more. If the panel marks the node as TLS, katana also needs a certificate in [node.controller.cert], and --test cannot know that because it does not contact the panel. Nodes covers what each node type needs.
The layout of the file
Section titled “The layout of the file”A complete file has four top-level sections. [log] and [dns] are process-wide, every [[node]] block is one panel node with its own sub-tables, and the [[outbound]] blocks form one pool that every node routes into.
[log] # process-wide: the log filter[dns] # process-wide: the resolver behind every outbound
[[node]] # one block per panel node[node.api] # how to reach the panel, and what the node is[node.controller] # bind address, poll interval, feature switches[node.controller.cert] # the certificate for TLS and Hysteria2 nodes[node.hysteria] # Hysteria2 listener settings[node.route] # this node's default outbound and geodata files[[node.route.rule]] # this node's routing rules, first match wins
[[outbound]] # one block per named upstream, shared by all nodesTOML attaches each sub-table to the most recent [[node]] above it, so write a node’s sub-tables directly after its [[node]] line and before the next one.
| Section | Scope | Documented on |
|---|---|---|
[log] |
process | this page |
[dns] |
process | this page |
[[node]], [node.api], [node.controller], [node.controller.cert] |
one node | Nodes |
[node.hysteria] |
one Hysteria2 node | Hysteria 2 nodes |
[node.route], [[node.route.rule]] |
one node | Routing |
[[outbound]] |
every node | Outbounds |
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
log | table | no | {} | Logging, written as [log]. Holds only level. Unlike etemenanki-app, katana applies a changed level on hot reload. |
dns | table | no | {} | Name resolution for the whole outbound pool, written as [dns]. Without it, katana uses the host resolver behind an always-on cache. The panel HTTP requests do not use it. |
node | array of tables | yes | — | The panel nodes to serve, one [[node]] block each, with the sub-tables [node.api], [node.controller], [node.controller.cert], [node.hysteria], [node.route] and [[node.route.rule]]. Written in the singular: [[nodes]] is an unknown field, and a single [node] table fails with invalid type: map, expected a sequence. The file parses without any, but both --test and a normal start fail with config defines no [[node]] entries. |
outbound | array of tables | no | [] | Named upstream proxies, one [[outbound]] block each, shared by every node. Written in the singular. The tags direct, freedom, block and blackhole always exist and cannot be redefined; a reserved or repeated tag fails with duplicate/reserved outbound tag <tag>. |
The diagram shows how the sections relate at run time. Each node has its own panel and its own routing table, but all routing tables pick from the same pool of outbounds, and every outbound resolves names through the same [dns] resolver.
flowchart LR P1["Panel A"] --> N1["node 1"] P2["Panel B"] --> N2["node 2"] N1 --> R1["node 1 route table"] N2 --> R2["node 2 route table"] R1 --> POOL["shared outbound pool"] R2 --> POOL POOL --> BUILTIN["direct, freedom, block, blackhole"] POOL --> OUT["outbound entries"] DNS["dns resolver"] -.-> POOL
There is no top-level [route]. Routing lives under each node as [node.route], and a top-level [route] table fails the parse as an unknown field.
How katana reads the file
Section titled “How katana reads the file”Strict parsing
Section titled “Strict parsing”Every table in the file rejects keys it does not know, and no key has an alias. A misspelt key stops katana instead of being skipped, so a typo cannot quietly leave a setting at its default. The error names the line, the key and the keys that table accepts:
configuration error: config parse error: TOML parse error at line 3, column 1 |3 | paneltype = "x" | ^^^^^^^^^unknown field `paneltype`, expected one of `panel_type`, `api`, `controller`, `route`, `hysteria`Values are typed just as strictly. node_id = "1" fails with invalid type: string "1", expected u32, node_id = -1 with invalid value: integer `-1`, expected u32, and level = 3 with invalid type: integer `3`, expected a string. The file must be UTF-8; anything else fails with config not utf-8.
Key names are snake_case and exactly as the reference tables list them. XrayR’s YAML used names such as ApiHost and NodeID; katana’s equivalents are host and node_id under [node.api]. Migrating from XrayR covers the differences.
Case in values
Section titled “Case in values”Some values that name a choice are matched without regard to case, so panel_type = "NewV2board" and panel_type = "newv2board" are the same. Others must be written exactly in lower case. The table lists every such value in the file:
| Value | Case | Example of an accepted spelling |
|---|---|---|
[[node]].panel_type |
any | "SSpanel", "NewV2board", "V2board" |
[node.api].node_type |
any | "V2ray", "Trojan", "Shadowsocks", "Hysteria2" |
[[outbound]].protocol |
any | "socks", "Shadowsocks", "WireGuard" |
[[outbound]].security |
any | "auto", "AES-128-GCM" |
[[outbound]].address_family |
any, and - counts as _ |
"ipv4_only", "IPv4-Only" |
[[outbound]].method, Shadowsocks 2022 names |
exact | "2022-blake3-aes-256-gcm" |
[[outbound]].method, other ciphers |
any | "aes-128-gcm", "CHACHA20-POLY1305" |
[dns].backend |
exact | "system", "udp", "tls", "https" |
[node.controller.cert].mode |
exact: only "file" loads a certificate |
"none", "file" |
[node.hysteria].credential, obfs |
exact | "uuid", "user_pass", "salamander" |
When katana reports an unknown panel_type, the message shows the value in lower case: panel_type = "XBoard" fails with unknown panel_type "xboard".
Relative paths
Section titled “Relative paths”Paths in the file, such as cert_file, key_file, geoip, geosite, rule_list_path and ca_file, are opened as written. A relative path is resolved against katana’s working directory, not against the directory of the config file. A systemd service runs in / unless its unit sets WorkingDirectory=, so use absolute paths such as /etc/katana/geoip.dat.
What --test checks
Section titled “What --test checks”--test builds as much of the configuration as it can without binding a port or contacting a panel. It stops at the first error, in this order:
- It reads and parses the file, which catches unknown keys, wrong types and invalid UTF-8.
- It reads
[dns].ca_file, if set, and builds the[dns]resolver. - It builds every
[[outbound]]and checks that no tag repeats or reuses a built-in name. - It checks that there is at least one
[[node]]. - For each node in turn, it checks
panel_typeandnode_type, then compiles[node.route]against the outbound pool, reading the geodata files that the rules use. For a Hysteria2 node it then checks[node.hysteria]and reads the certificate and key.
Everything the panel decides is out of its reach. --test does not check that the panel URL and key work, that the port the panel assigns is free, or that a TLS node’s certificate files exist. It also does not check [log].level or read rule_list_path. Those problems show up in the log when the node starts.
The [log] section
Section titled “The [log] section”[log] sets which log lines katana writes to standard output.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
level | string | no | "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 "info,katana=debug". At startup a RUST_LOG variable that holds a valid filter takes precedence. --test does not validate it: at startup an unparsable directive is skipped with an ignoring ... line on stderr, and a word that is not a level, such as "warning", is read as a target name and silences every log line, errors included. A hot reload that changes the value applies it and replaces the RUST_LOG filter; a value that does not parse is logged as invalid log level and the current filter stays. |
[log]level = "info,katana=debug"At startup, katana takes its filter from the RUST_LOG environment variable if that variable holds a valid filter, and from [log].level otherwise. RUST_LOG=debug katana -c /etc/katana/config.toml is a quick way to get more detail from one run without editing the file.
Unlike etemenanki-app, katana applies a changed level on hot reload. When a reload finds a different level, katana installs it, logs reload: log level → <level> at info level, and from then on the file wins over RUST_LOG. If the new value does not parse, katana logs invalid log level and keeps the current filter. Removing the key sets the level back to info. A reload that does not change level leaves the current filter alone, including one that came from RUST_LOG.
The [dns] section
Section titled “The [dns] section”[dns] chooses how the outbound pool turns host names into addresses. It is the same resolver etemenanki-app uses, configured with the same keys and the same rules; DNS describes the resolver itself in more depth.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
backend | string (enum) | no | "system" | Where answers come from: "system" (the host resolver, which honours /etc/hosts and nsswitch.conf), "udp" (plain DNS), "tls" (DNS over TLS, RFC 7858) or "https" (DNS over HTTPS, RFC 8484). Case-sensitive, unlike most katana enums: "UDP" fails with dns: unknown backend "UDP" (expected "system", "udp", "tls" or "https"). |
server | string | depends | — | The resolver address as an IP literal with a port, for example "192.0.2.53:53" or "[2001:db8::53]:853". Required for udp, tls and https; without it the build fails with dns: the <backend> backend needs a server address. A host name or a missing port fails with dns: invalid server address: invalid socket address syntax. For https this is the address katana dials; the host in url is not resolved. Ignored by system. |
server_name | string | depends | — | The name the resolver certificate is verified against. Required for tls; without it the build fails with dns: the tls backend needs a server name to verify against. Ignored by the other backends. |
url | string | depends | — | The DNS over HTTPS endpoint, for example "https://dns.example.com/dns-query". Required for https; without it the build fails with dns: the https backend needs the resolver's url. It must start with https://, otherwise dns: "<url>" is not an https:// url. Its host is the name the certificate is verified against and the Host header; a port in it is ignored, and a URL without a path uses /dns-query. A URL with a user part fails with has no usable host. Ignored by the other backends. |
ca_file | path | no | — | A PEM bundle of extra CA certificates for verifying a tls or https resolver. katana adds them to the system roots rather than replacing them. katana reads it whenever the key is set, even for system and udp, which do not use it. A relative path is resolved against the working directory. A missing file fails with the bare No such file or directory (os error 2), which does not name the path. With tls or https, a file that holds no certificate fails with no certificate in CA PEM bundle. |
# The default; the same as leaving [dns] out.[dns]backend = "system"The host resolver, through getaddrinfo. It is the only backend that honours /etc/hosts, nsswitch.conf and search domains.
[dns]backend = "udp"server = "192.0.2.53:53"Plain DNS over UDP to one server.
[dns]backend = "tls"server = "192.0.2.53:853"server_name = "dns.example.com"DNS over TLS. katana dials server and verifies its certificate against server_name.
[dns]backend = "https"server = "192.0.2.53:443"url = "https://dns.example.com/dns-query"DNS over HTTPS. katana dials server and verifies the certificate against the host in url, so the resolver’s own name never has to be resolved.
To trust a resolver whose certificate comes from a private CA, add ca_file to a tls or https backend. katana adds the certificates in the file to the system roots; it does not replace them, so a resolver with a publicly trusted certificate still passes:
[dns]backend = "tls"server = "192.0.2.53:853"server_name = "dns.example.com"ca_file = "/etc/katana/dns-ca.pem"A key that the chosen backend does not use is not checked. With backend = "system", a leftover server = "garbage" passes --test. The exception is ca_file, which katana reads whenever it is set.
Who uses the resolver
Section titled “Who uses the resolver”katana builds one resolver, with one cache, and hands it to every outbound in the pool, the built-in direct and freedom included. It resolves:
- the destinations of traffic that leaves through
direct,freedomor aprotocol = "direct"outbound; - the
servernames of upstream proxy outbounds; - the destinations of traffic sent through a WireGuard outbound.
A WireGuard outbound’s own server, the peer endpoint, is the exception: katana resolves that name with the host resolver.
Because every node routes into the same pool, every node shares this cache. The panel requests do not go through it: katana’s HTTP client for the panel API uses the host resolver, whatever [dns] says.
Answers are cached for up to 8192 names. Host-resolver answers carry no TTL, so katana keeps them for 60 seconds; answers from the other backends are kept for their TTL, clamped to between 5 seconds and 1 hour. With udp, tls and https, each query times out after 5 seconds.
Several nodes in one file
Section titled “Several nodes in one file”One katana process can serve any number of panel nodes, from the same panel or from different panels. Repeat the [[node]] block once per node, each followed by its own sub-tables. The nodes run independently: each has its own panel client, poll timer, listener, user table, traffic counters and routing table, and a node whose panel is unreachable does not affect the others.
A node that cannot come up the first time, because its panel request for the node or its users fails or because its listener cannot bind, logs node <id>: <reason>; retrying in <N>s and tries again. The first wait is 1 second, and each failure after that doubles it, up to 60 seconds or the node’s update_periodic, whichever is shorter. The other nodes keep running meanwhile. A reload that changes the waiting node’s [[node]] block makes it try again at once, since the edit may be the fix.
| Shared by all nodes | Separate for each node |
|---|---|
[log] |
panel_type and everything in [node.api] |
[dns] and its cache |
[node.controller] and its certificate |
the [[outbound]] pool, including direct and block |
[node.hysteria] |
| the process, its file watcher and its signals | [node.route] and its rules |
The example below serves an Xboard VMess node and an SSPanel Trojan node from one process. The VMess node sends private addresses to block, example.org and its subdomains to the relay outbound and everything else to direct. The Trojan node sends everything to relay except the RFC 1918 private ranges and port 25 (SMTP), which go to block. Both nodes use the same relay outbound, defined once.
# Two panel nodes in one katana process: an Xboard (newV2board) VMess node and# an SSPanel Trojan node. Both share the [dns] resolver and the [[outbound]]# pool; each has its own [node.route] table.## Validate with: katana --test -c /etc/katana/config.toml
[log]level = "info"
# One resolver and cache for every outbound in the pool.[dns]backend = "tls"server = "192.0.2.53:853"server_name = "dns.example.com"
# ---------------------------------------------------------------------------# Node 1: VMess from an Xboard panel. The panel supplies the port, transport# and TLS flag; the certificate is always local.# ---------------------------------------------------------------------------[[node]]panel_type = "NewV2board"
[node.api]host = "https://panel.example.com"node_id = 1key = "replace-with-the-panel-key"node_type = "V2ray"timeout = 10
[node.controller]listen_ip = "0.0.0.0"update_periodic = 60
[node.controller.cert]mode = "file"cert_file = "/etc/katana/proxy.example.com.crt"key_file = "/etc/katana/proxy.example.com.key"
[node.route]default = "direct"geoip = "/etc/katana/geoip.dat"
[[node.route.rule]]outbound = "block"geoip = ["private"]
[[node.route.rule]]outbound = "relay"domain_suffix = ["example.org"]
# ---------------------------------------------------------------------------# Node 2: Trojan from an SSPanel panel. Trojan always runs over TLS.# ---------------------------------------------------------------------------[[node]]panel_type = "SSpanel"
[node.api]host = "https://sspanel.example.com"node_id = 2key = "replace-with-the-panel-key"node_type = "Trojan"
[node.controller]update_periodic = 60
[node.controller.cert]mode = "file"cert_file = "/etc/katana/proxy.example.com.crt"key_file = "/etc/katana/proxy.example.com.key"
[node.route]default = "relay"
[[node.route.rule]]outbound = "block"cidr = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
[[node.route.rule]]outbound = "block"port = ["25"]
# ---------------------------------------------------------------------------# The shared outbound pool. `direct`, `freedom`, `block` and `blackhole` are# built in; these entries add to them.# ---------------------------------------------------------------------------[[outbound]]tag = "relay"protocol = "shadowsocks"server = "198.51.100.20"port = 8388method = "2022-blake3-aes-256-gcm"# Generate a real key with: openssl rand -base64 32password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="The Shadowsocks 2022 key in the relay outbound is a placeholder. Generate a real one with openssl rand -base64 32.
Each node binds one port on its listen_ip. The panel assigns that port, except for a Hysteria2 node with [node.hysteria].port set, which uses that port and does not ask the panel for one. Two nodes that bind the same TCP port on the same address cannot both start: whichever binds second logs node <id>: initial start failed: …; retrying in <N>s and keeps retrying, so it comes up once the port is free. A Hysteria2 node listens on UDP, so it can use the same port number as a TCP node. --test does not check ports.
How katana tells nodes apart
Section titled “How katana tells nodes apart”On a hot reload, katana matches each [[node]] in the new file to a running node by the node’s identity: the combination of
panel_type, compared without regard to case;[node.api].host, compared as written;[node.api].node_id;[node.api].key;- on NewV2board and V2board, the node type katana asks the panel for:
vlessfor aV2ray,VmessorVlessnode withenable_vless = true, and otherwisenode_typein lower case. SSPanel finds a node by its ID alone, so an SSPanel identity has no node type.
The node type is part of the identity because Xboard and V2board look a node up by its ID and the type it is asked for. One node_id asked for as vless and as vmess is two panel nodes, each with its own users and its own traffic, and katana treats it the same way.
When the identity of an entry is unchanged, katana reconfigures the running node in place. That includes the other [node.api] keys, such as timeout and speed_limit: katana builds a new panel client from the edited block and asks the panel for the node and its users at once. When any part of the identity changes, katana treats the edit as removing one node and adding another: it stops the old node, which reports the traffic it has counted one last time, and starts a new one that contacts the panel from scratch. The order of the [[node]] blocks in the file does not matter. Hot reload lists what each kind of change does to a running node.
The host is compared as a string, so https://panel.example.com and https://panel.example.com/ count as different identities even though both reach the same panel.
What a reload does with each section
Section titled “What a reload does with each section”katana watches the directory that holds the config file. Any change in that directory triggers a reload about half a second later, and katana then compares the new file with the running one section by section. A file that does not parse is ignored as a whole, with config reload failed, keeping current: … in the log. For a file that parses, each top-level section is handled differently:
| Section | On reload |
|---|---|
[log] |
A changed level is applied at once, unless the reload is rejected for its outbounds or its nodes. |
[dns] |
Applied only when the same reload also changes an [[outbound]]. |
[[outbound]] |
The pool is rebuilt. If it fails to build, the whole reload is rejected with reload: bad outbounds, keeping current config: … and nothing else is applied. If it builds, every node recompiles its routing table and restarts its listener, which drops every connection on every node. A node whose rules no longer compile against the new pool logs node <id>: route rebuild failed, keeping current: … and keeps its old routing table, but still restarts its listener. |
[[node]] |
Before anything is applied, katana builds every node the file adds, with its panel client and routing table, and the panel client of every node whose block changed. If any of them does not build, the whole reload is rejected with reload: node <name>: <error>; keeping current config and nothing else is applied. Otherwise new identities are started, missing ones are stopped, and changed ones are reconfigured live, [node.api] keys included, through a new panel client. A changed [node.route] that does not compile is refused by that node alone: it logs node <id>: config edit refused, keeping the running one: … and keeps all its old settings, while the rest of the reload applies. |
A reload that leaves no [[node]] entries in the file stops every node, and katana keeps running with none. An empty file parses, so emptying the file has the same effect. The “at least one node” rule applies only at startup and to --test.
Common errors
Section titled “Common errors”| Message | Cause | Fix |
|---|---|---|
unknown field `route`, expected one of `log`, `dns`, `node`, `outbound` |
A top-level [route], or another stray section. |
Move routing under each node as [node.route]. |
unknown field `nodes` |
[[nodes]] in the plural. |
Write [[node]] and [[outbound]] in the singular. |
invalid type: map, expected a sequence |
A node written as [node] instead of [[node]]. |
Use double brackets, even for a single node. |
config defines no [[node]] entries |
No [[node]] block. |
Add at least one node. |
unknown panel_type "" |
A [[node]] without panel_type. |
Set panel_type to SSpanel, NewV2board or V2board. |
unknown panel_type "xboard" |
The panel’s product name instead of its API type. | Xboard and V2board speak the newV2board API: use NewV2board. |
unknown node_type "vmess2" |
A node_type katana does not know. |
Use V2ray, Trojan, Shadowsocks or Hysteria2. |
duplicate/reserved outbound tag direct |
An outbound tag used twice, or one of direct, freedom, block, blackhole. |
Rename the outbound. To give direct traffic its own settings, add protocol = "direct" under a new tag. |
route references unknown outbound tag: x |
A route default or rule outbound that names no outbound. This message does not say which node. |
Fix the tag, or add the missing [[outbound]]. |
dns: unknown backend "UDP" (expected …) |
A backend in the wrong case, or a name such as "doh". |
Use system, udp, tls or https in lower case. |
dns: invalid server address: invalid socket address syntax |
A host name, or an address without a port, in [dns].server. |
Write an IP address and port, such as "192.0.2.53:53". |
No such file or directory (os error 2) |
The config file itself, or a file named in it, is missing: [dns].ca_file, a geodata file, or a Hysteria2 certificate. The message does not say which. |
Check every path, and use absolute paths. |
Troubleshooting covers errors that appear only once a node is running.