Skip to content

Outbounds

An outbound is where katana sends a user’s traffic after the node has accepted it. By default that is direct: katana connects to the destination from its own host. With [[outbound]] tables you add named upstreams, such as a SOCKS5 or HTTP proxy, a VMess, VLESS or Shadowsocks server, or a WireGuard peer. A node’s route rules then pick one of them per connection or per UDP packet.

This page covers the outbound pool, every [[outbound]] key, what each protocol can carry, a complete WireGuard egress example, and how to check an upstream before you put it in front of users. Which traffic goes where is decided by the route table; see Routing.

katana builds a single outbound pool for the whole process. Every [[node]] shares it, and each node’s [node.route] refers to outbounds by tag. An outbound used by several nodes is one object: a WireGuard outbound, for example, is one tunnel that carries the traffic of every node and user routed to it.

flowchart LR
  A["node 1 route"] --> P
  B["node 2 route"] --> P
  P["outbound pool"] --> D["direct / freedom"]
  P --> K["block / blackhole"]
  P --> S["socks-up"]
  P --> W["wg-egress"]
  D --> I["destination"]
  S --> U["upstream proxy"]
  W --> R["WireGuard peer"]

The pool always contains four built-in tags, whether or not you declare any [[outbound]]:

Tag Alias What it does
direct freedom Connects to the destination from this host, with address_family = "auto". It is also the route default: a node without [node.route].default sends unmatched traffic here.
block blackhole Refuses a TCP connection and drops a UDP packet. A dropped packet is not billed to the user.

You cannot redefine these tags. Declaring [[outbound]] with tag = "direct" fails the build, and so does declaring the same tag twice:

configuration error: duplicate/reserved outbound tag direct

Tags are compared case-sensitively. Direct is not a reserved tag and is accepted as a new outbound, and a route that says outbound = "Up" does not find an outbound tagged up: it fails with route references unknown outbound tag: Up.

This config forwards one domain’s traffic to an upstream SOCKS5 proxy and sends everything else directly:

config.toml
[[node]]
panel_type = "NewV2board"
[node.api]
host = "https://panel.example.com"
node_id = 1
key = "replace-with-the-panel-key"
node_type = "V2ray"
[[node.route.rule]]
outbound = "socks-up"
domain_suffix = ["example.com"]
[[outbound]]
tag = "socks-up"
protocol = "socks"
server = "192.0.2.20"
port = 1080
username = "katana"
password = "replace-with-a-long-random-password"

[[outbound]] is a top-level table, like [[node]]. You can put it anywhere in the file as long as it starts on its own header line. Run katana --test -c config.toml after every edit. It builds the whole pool and every node’s route table, so a bad key, a missing field or an unknown tag fails there, before a running katana ever sees it.

Every [[outbound]] table is strict: an unknown key, such as user instead of username, fails the parse with unknown field and the list of accepted keys. A key that the chosen protocol does not use, for example uuid on a socks outbound, is accepted and ignored.

KeyTypeRequiredDefaultDescription
tagstringyes—Name that [node.route].default and [[node.route.rule]].outbound refer to. Must be unique across all [[outbound]] tables and must not be one of the built-in tags direct, freedom, block or blackhole; either mistake fails with duplicate/reserved outbound tag <tag>. Compared case-sensitively everywhere.
protocolstring (enum)yes—Which client to build, case-insensitive: socks (alias socks5), http, vmess, vless, shadowsocks (alias ss), wireguard (alias wg), or direct (alias freedom). Anything else fails with unknown outbound protocol "<name>", with the name in lower case. There is no trojan, hysteria2 or blackhole protocol here; use the built-in block tag to drop traffic.
serverstringdepends—The upstream host: an IP address or a domain name. For wireguard it is the peer's UDP endpoint host. Required for every protocol except direct, which ignores it. Write an IPv6 address without brackets. A domain is resolved with the [dns] resolver when a connection is made, except for wireguard, whose endpoint name goes through the system resolver when the tunnel starts. Empty fails with outbound needs a non-empty server and non-zero port.
portu16depends—The upstream port; the peer's UDP port for wireguard. Required and non-zero for every protocol except direct. Zero or absent fails with outbound needs a non-empty server and non-zero port (got "<server>":0). This check runs before protocol is read, so it also catches a misspelled protocol that has no server or port.
usernamestringno—socks and http only. SOCKS5 username/password authentication, or HTTP Basic Proxy-Authorization. Used only when password is also set; with one of the two missing, the client does not authenticate at all.
passwordstringdepends—For socks and http, the password that goes with username. For shadowsocks it is required and holds the secret: the password for a classic method, or the base64 PSK for a 2022- method, optionally as an iPSK:uPSK chain. Missing for shadowsocks fails with shadowsocks outbound <tag> needs a password. A 2022 key shorter than the method's key length fails with shadowsocks-2022: PSK too short (…), and invalid base64 with decode PSK: ….
uuidstringdepends—Required for vmess and vless: the account UUID on the upstream server. Missing fails with outbound <tag> needs a uuid; a malformed value fails with outbound <tag>: uuid is not a valid UUID, which does not repeat the value, because it is a credential.
securitystring (enum)no"auto"vmess only. The body cipher, case-insensitive: auto, aes-128-gcm or aes128gcm select AES-128-GCM; chacha20-poly1305 or chacha20poly1305 select ChaCha20-Poly1305. auto always means AES-128-GCM. none, zero and anything else fail with unsupported vmess security "<name>".
methodstring (enum)depends—Required for shadowsocks; the name picks the generation. SIP022: 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm, 2022-blake3-chacha20-poly1305, written exactly in lower case. Classic AEAD, case-insensitive: aes-128-gcm, aes-256-gcm, chacha20-poly1305 (or chacha20-ietf-poly1305), xchacha20-poly1305 (or xchacha20-ietf-poly1305), and the aead_aes_128_gcm, aead_aes_256_gcm, aead_chacha20_poly1305 aliases. Anything else, including an empty or missing value, fails with unsupported shadowsocks cipher "<name>".
global_paddingboolnofalsevmess only. Sets the VMess global padding option: each chunk carries 0 to 63 bytes of padding, which hides the exact payload sizes at a small cost in bandwidth.
address_familystring (enum)no"auto"Which resolved addresses the outbound may use, for any protocol: auto (keep the resolver's order), ipv4_only, ipv6_only, prefer_ipv4, prefer_ipv6. Case-insensitive, surrounding spaces are ignored, - is read as _, an empty string means auto, and these aliases are accepted: ipv4, v4, 4, ipv4only; ipv6, v6, 6, ipv6only; prefer_v4, ipv4_prefer, v4_prefer; prefer_v6, ipv6_prefer, v6_prefer. It applies to the destination for direct and wireguard, and to the server name for the other protocols. Anything else fails with outbound <tag> invalid address_family "<value>".
private_keystringdepends—Required for wireguard: your own Curve25519 private key, the PrivateKey line of a wg-quick file. 32 bytes as padded standard base64 (what wg genkey prints) or 64 hex digits. Missing fails with wireguard outbound <tag> needs a private_key; a bad encoding with wireguard outbound <tag> private_key: invalid WireGuard key: expected base64 or hex encoding of 32 bytes.
public_keystringdepends—Required for wireguard: the peer's public key, the PublicKey line of the [Peer] section. Same encodings and error forms as private_key. etemenanki-app calls this key peer_public_key.
pre_shared_keystringno—wireguard only. Optional pre-shared key, the PresharedKey line. Same encodings as private_key; a bad one fails with wireguard outbound <tag> pre_shared_key: …. Set it only when the peer has one for you. etemenanki-app spells it preshared_key.
local_addressarray of stringsdepends—Required for wireguard, at least one entry: the tunnel-local addresses from the Address line, such as ["10.8.0.2/32", "2001:db8:a::2/128"]. Anything from / on is dropped, so the prefix length is optional. Without an IPv6 entry the tunnel cannot reach IPv6 destinations, and the other way round. Errors: wireguard outbound <tag> needs at least one local_address, wireguard outbound <tag> invalid local_address "<value>": ….
mtuintegerno1420wireguard only. Largest IP packet inside the tunnel, in bytes; the userspace TCP stack sizes its segments from it. Not range-checked. Lower it (for example to 1280) when small requests work but large transfers stall.
keepaliveu16no—wireguard only. Persistent keepalive interval in seconds, the PersistentKeepalive line. Absent or 0 turns it off. Set it (commonly 25) when this host is behind NAT or a stateful firewall.
reservedarray of integersno—wireguard only. Exactly three bytes, each 0 to 255, written into header bytes 1 to 3 of every outgoing WireGuard packet, as Xray's reserved does. Set it only when your service tells you to. Another length fails with wireguard outbound <tag> reserved must be exactly 3 bytes; a value above 255 fails the parse.

Which keys each protocol reads:

protocol Required Optional
direct, freedom none address_family
socks, socks5 server, port username and password, address_family
http server, port username and password, address_family
vmess server, port, uuid security, global_padding, address_family
vless server, port, uuid address_family
shadowsocks, ss server, port, method, password address_family
wireguard, wg server, port, private_key, public_key, local_address pre_shared_key, mtu, keepalive, reserved, address_family

Nodes relay UDP as well as TCP when their protocol carries it. katana routes each UDP packet on its own, so one client’s UDP traffic can reach several outbounds. Not every outbound can carry UDP:

Outbound TCP UDP
direct Yes Yes. One socket per address family, each packet to its own destination.
block Refused Dropped
socks Yes Yes, through SOCKS5 UDP ASSOCIATE. Each packet carries its own destination.
http Yes, through CONNECT No
vmess, vless Yes Yes, with one fixed target per client association (see below).
shadowsocks (both generations) Yes No
wireguard Yes Yes. Each packet carries its own destination.

When a UDP packet is routed to an outbound that carries no UDP, katana drops it. The client sees no error, only silence, so do not route DNS or QUIC to http or shadowsocks outbounds.

The built-in direct tag uses address_family = "auto". To send direct traffic with a different address-family policy, declare your own direct outbound under a new tag and route to it:

[[outbound]]
tag = "direct-v4"
protocol = "direct"
address_family = "ipv4_only"

A direct outbound needs no server or port and ignores them if present. For TCP, katana resolves the destination and tries every allowed address in turn until one connects. For UDP, it binds a socket for each allowed family, resolves each domain target once per association and sends to the first allowed address, and drops packets to a name with no usable address.

socks speaks SOCKS5 and http speaks HTTP CONNECT. Authentication is used only when both username and password are set: SOCKS5 username/password authentication, or an HTTP Basic Proxy-Authorization header. With only one of the two set, katana connects without credentials and the upstream decides whether to accept that.

Both need the account uuid from the upstream server. VMess uses the AEAD header format with security choosing the body cipher; auto is AES-128-GCM, and there is no unencrypted mode. VLESS sends an empty flow, so an Xray upstream whose account requires xtls-rprx-vision rejects its TCP connections.

The method name decides which generation katana speaks, and password changes meaning with it:

[[outbound]]
tag = "ss-up"
protocol = "shadowsocks"
server = "198.51.100.30"
port = 8388
method = "2022-blake3-aes-256-gcm"
password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="

password is a base64 key of at least the method’s key length: 16 bytes for 2022-blake3-aes-128-gcm, 32 bytes for the other two. Generate one with openssl rand -base64 32 (or -base64 16). A shorter key fails with shadowsocks-2022: PSK too short (16 < 32), and invalid base64 fails with decode PSK: ….

For a multi-user server, write the identity key and your user key separated by a colon, iPSK:uPSK. The last key is always yours; the ones before it form the identity chain. Each part is decoded and length-checked on its own. The multi-user extension (SIP023) defines identity keys for the two AES methods only, so use a chain only with those.

The 2022 method names must be written exactly, in lower case: 2022-BLAKE3-AES-128-GCM fails with unsupported shadowsocks cipher. Classic names are case-insensitive. Stream ciphers such as rc4-md5 are not supported.

A wireguard outbound runs a userspace WireGuard tunnel inside katana: no network interface, no host routes, no root. server and port are the peer’s UDP endpoint. It carries TCP and UDP. The rest of this section explains how it differs from the other outbounds; the WireGuard example below shows a complete node.

  • Lazy start. Building the config opens nothing, so --test checks the key encodings but never contacts the peer. The first flow routed to the outbound resolves the endpoint, binds a UDP socket and starts the tunnel, and that first connection waits for the handshake.
  • Endpoint resolution. A domain in server is resolved once per tunnel start with the host’s system resolver, not [dns], and the first answer is used.
  • Destination resolution. When a flow through the tunnel names its destination by domain, katana resolves the name with its own [dns] resolver on this host and connects to the IP through the tunnel. The lookup itself does not travel through the tunnel.
  • Address families. The tunnel can reach only the families it has a local_address for. With address_family = "ipv4_only" you need an IPv4 local_address, and with ipv6_only an IPv6 one; otherwise the build fails with wireguard outbound <tag> address_family ipv6_only needs an IPv6 local_address. A connection to a destination with no usable address fails with wireguard: no usable <policy> destination address for …, followed by the families the tunnel supports.
  • One stalled destination. Each flow through the tunnel has its own bounded buffers. When a destination stops reading, its flow’s buffers fill and then only that flow’s sender waits, so katana reads no more of that flow’s upload from the client until the destination reads again. The other flows, users and nodes that share the tunnel keep moving. A slow tunnel or peer likewise pushes back on the senders, which slow down to the pace the tunnel accepts. A UDP datagram that is larger than its association’s send buffer, or whose destination the tunnel cannot address, is dropped, and the rest of the association keeps flowing.
  • Recovery. If the tunnel’s driver stops, for example because its UDP socket reports an error, the next flow builds a new tunnel and resolves the endpoint again. A tunnel that had run for at least 10 seconds is rebuilt at once. If it died sooner, or a start failed (for example, the endpoint name did not resolve), katana waits 2 seconds before the next attempt, doubling up to 30 seconds, and flows that arrive in the meantime fail with wireguard: tunnel is down, waiting before the next attempt.

address_family works for every protocol, but it filters different addresses depending on who resolves what:

Outbound address_family applies to
direct The destination’s resolved addresses.
socks, http, vmess, vless, shadowsocks The resolved addresses of server. The upstream resolves the destination itself.
wireguard The destination’s resolved addresses, further limited to the families of local_address. The endpoint in server is not affected.
Value Effect
auto Keep the resolver’s order. TCP tries every address in turn; UDP uses the first usable one.
ipv4_only, ipv6_only Use only that family.
prefer_ipv4, prefer_ipv6 Try that family first and keep the other as a fallback.

This node sends two domains and one network through a WireGuard peer, blocks one domain, and relays everything else directly. The destinations routed to wg-egress see the peer’s address instead of the node’s.

config.toml
# katana: one newV2board node whose route sends selected traffic out through
# a WireGuard tunnel, blocks one domain, and relays everything else directly.
# Keys are placeholders: generate your own with `wg genkey | tee private.key | wg pubkey`.
[log]
level = "info"
[[node]]
panel_type = "NewV2board"
[node.api]
host = "https://panel.example.com"
node_id = 1
key = "replace-with-the-panel-key"
node_type = "V2ray"
[node.route]
default = "direct"
# First match wins. Traffic for these domains and this network leaves
# through the tunnel, so the destination sees the WireGuard peer's address.
[[node.route.rule]]
outbound = "wg-egress"
domain_suffix = ["example.com", "example.org"]
cidr = ["198.51.100.0/24"]
[[node.route.rule]]
outbound = "block"
domain_suffix = ["ads.example.net"]
# The tunnel. `server` and `port` are the peer's UDP endpoint
# (wg-quick's `Endpoint`); `public_key` is the peer's key.
[[outbound]]
tag = "wg-egress"
protocol = "wireguard"
server = "203.0.113.10"
port = 51820
private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
local_address = ["10.8.0.2/32", "2001:db8:a::2/128"]
address_family = "prefer_ipv4"
mtu = 1420
keepalive = 25

The keys in the file are placeholders. Generate a real key pair with wg genkey | tee private.key | wg pubkey, and register the public half with the peer, or use the keys your WireGuard service gives you.

WireGuard services usually hand out a wg-quick file. Both tabs describe the same tunnel:

[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.8.0.2/32, 2001:db8:a::2/128
DNS = 192.0.2.53
MTU = 1280
[Peer]
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
PresharedKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25

etemenanki-app builds the same tunnel from differently named keys, nested under [outbound.settings]. The last column matters when you test an upstream with etemenanki-app first, as described in the next section:

wg-quick katana [[outbound]] etemenanki-app [outbound.settings]
[Interface] PrivateKey private_key private_key
[Interface] Address local_address, prefix allowed: ["10.8.0.2/32"] address, bare IPs only: ["10.8.0.2"]
[Interface] MTU mtu mtu
[Peer] PublicKey public_key peer_public_key
[Peer] PresharedKey pre_shared_key preshared_key
[Peer] Endpoint server and port, separately endpoint = "203.0.113.10:51820"
[Peer] PersistentKeepalive keepalive keepalive
[Peer] AllowedIPs none: route rules choose what enters the tunnel none
[Interface] DNS none: [dns] resolves names on the host none
none reserved reserved

Copying Address into etemenanki-app with its prefix fails with invalid IP address syntax, and a peer_public_key key in a katana outbound fails with unknown field. Both programs refuse the wrong spelling rather than ignoring it.

katana --test proves only that the outbound builds. It cannot tell you whether the upstream accepts your credentials or forwards your traffic, and a wrong WireGuard key looks exactly like an unreachable peer: connections time out. Before you route users to a new upstream, run it once through a throwaway etemenanki-app on the same host. It shares nothing with the running katana: separate process, separate config file, loopback port.

  1. Translate the outbound into etemenanki-app’s format and give it a loopback SOCKS inbound. For the WireGuard outbound above:

    wg-check.toml
    # Throwaway check of one WireGuard upstream through a loopback SOCKS proxy.
    [[inbound]]
    tag = "check"
    protocol = "socks"
    listen = "127.0.0.1"
    port = 10808
    [[outbound]]
    tag = "wg-check"
    protocol = "wireguard"
    address_family = "prefer_ipv4"
    [outbound.settings]
    private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
    peer_public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
    endpoint = "203.0.113.10:51820"
    address = ["10.8.0.2", "2001:db8:a::2"]
    mtu = 1420
    keepalive = 25

    etemenanki-app sends unmatched traffic to its first outbound, so with a single outbound everything goes through it. For a SOCKS, HTTP, VMess, VLESS or Shadowsocks upstream, use the matching etemenanki-app outbound from Outbounds instead, with no TLS, WebSocket or gRPC transport, because katana reaches the upstream over plain TCP.

  2. Check the file with etemenanki-app --test -c wg-check.toml, then start it in the foreground with etemenanki-app -c wg-check.toml.

  3. From a second terminal, ask an IP echo service which address it sees:

    Terminal window
    curl -m 30 --socks5-hostname 127.0.0.1:10808 https://ifconfig.me

    The answer should be the upstream’s exit address, not this host’s. If the first request through a new WireGuard tunnel times out, try once more before you conclude anything: the first connection waits for the handshake.

  4. Stop etemenanki-app with Ctrl-C, copy the same values into katana’s [[outbound]] using katana’s key names, and run katana --test.

katana watches its config file. When you save a change to any [[outbound]] table:

  1. katana builds the whole new pool first. If any outbound fails to build, it logs reload: bad outbounds, keeping current config: … and applies nothing from that edit, including node and log changes. The running pool keeps working.
  2. Before it touches a running node, katana builds every node the edit adds, panel client and route table, against the new pool, and the panel client of every node whose settings changed. If one of them fails, it logs reload: node <node>: <error>; keeping current config and again applies nothing from that edit. <node> is the panel type, host, node ID and, on NewV2board and V2board, the node type, for example newv2board@https://panel.example.com#1/v2ray. So a new node whose route names a tag the new pool lacks rejects the whole edit, with build router: route references unknown outbound tag: … as the error.
  3. If everything builds, every node recompiles its route table against the new pool and rebuilds its listener. This drops every open connection on every node, and every WireGuard tunnel starts over with a new handshake. A node that is still retrying its first start has no listener yet: it recompiles its routes against the new pool and uses them at its next attempt.
  4. If, after the edit, a node’s route still names a tag the new pool no longer has, that node logs node <id>: route rebuild failed, keeping current: … and keeps routing with its previous table, which still points at the old outbounds. katana --test reports the same mistake as route references unknown outbound tag.

The [dns] resolver is built together with the pool, so a change to [dns] alone takes effect only at the next outbound change or restart. See Hot reload for everything a reload does.

At startup, a pool that fails to build stops katana with failed to build outbounds: … and exit status 1.

Error Cause and fix
duplicate/reserved outbound tag <tag> The tag is direct, freedom, block or blackhole, or two [[outbound]] tables share it. Rename one.
route references unknown outbound tag: <tag> A route rule or default names a tag that is not in the pool. Check the spelling and case.
unknown outbound protocol "<name>" protocol is not one of the values in the settings table. The name is shown in lower case. Trojan and Hysteria 2 upstreams are not supported.
outbound needs a non-empty server and non-zero port (got …) server or port is missing on a protocol that needs an upstream. katana checks this before it looks at protocol, so a misspelled protocol without server and port also fails here.
outbound <tag> needs a uuid A vmess or vless outbound has no uuid.
outbound <tag>: uuid is not a valid UUID The uuid is malformed. The message leaves the value out on purpose.
unsupported vmess security "<name>" Use auto, aes-128-gcm or chacha20-poly1305.
shadowsocks outbound <tag> needs a password Add password.
unsupported shadowsocks cipher "<name>" method is missing, misspelled, or a 2022 name in the wrong case.
shadowsocks-2022: PSK too short (16 < 32) The key decodes to fewer bytes than the method needs. Generate one with openssl rand -base64 32.
outbound <tag> invalid address_family "<value>" Use one of the values in Address family.
wireguard outbound <tag> needs a private_key (or public_key) Add the missing key.
wireguard outbound <tag> private_key: invalid WireGuard key: … The key is not 32 bytes of padded base64 or hex. Copy it again, including the trailing =.
wireguard outbound <tag> needs at least one local_address Copy the Address line from the wg-quick file.
wireguard outbound <tag> reserved must be exactly 3 bytes Write three numbers, for example reserved = [1, 2, 3], or remove the key.
unknown field …, expected one of … A misspelled key, or an etemenanki-app key name such as peer_public_key or endpoint.