Inbounds
An inbound is a listener that accepts connections from clients in one protocol and hands every flow it decodes to routing. Each [[inbound]] table in the config file creates one. A config can have as many inbounds as you need, in any mix of protocols, and each is identified by its tag.
This page covers the keys every inbound shares, what each protocol can carry, how listen and port decide what gets bound, Unix-socket listeners, and the fixed limits on every listener. The protocol-specific [inbound.settings] keys are on each protocol’s own page.
katana does not read [[inbound]] tables: there, the panel decides each node’s protocol, port and transport. Everything below is about etemenanki-app.
A minimal inbound
Section titled “A minimal inbound”A SOCKS proxy for programs on the same machine:
[[inbound]]tag = "socks-in"protocol = "socks"port = 1080 # listen defaults to 127.0.0.1
[[outbound]]tag = "direct"protocol = "freedom"Because listen is not set, this binds 127.0.0.1:1080 and is reachable only from the local host. At startup the log says so:
INFO etemenanki_app::instance: inbound socks-in listening on 127.0.0.1:1080A server usually carries more than one inbound. This config serves a local SOCKS proxy, a public VLESS endpoint over WebSocket and TLS, and an HTTP proxy on a Unix socket for a local service:
[[inbound]]tag = "local-socks"protocol = "socks"port = 1080 # 127.0.0.1 only
[[inbound]]tag = "public-vless"protocol = "vless"listen = "0.0.0.0" # every IPv4 interfaceport = 443
[inbound.stream]network = "ws"security = "tls"
[inbound.stream.ws]path = "/ray"
[inbound.stream.tls]cert_file = "/etc/etemenanki/cert.pem"key_file = "/etc/etemenanki/key.pem"
[inbound.settings]users = [{ id = "11111111-2222-3333-4444-555555555555" }]
[[inbound]]tag = "app-http"protocol = "http"listen = "/run/etemenanki/http.sock" # a Unix socket: no port
[[outbound]]tag = "direct"protocol = "freedom"Check a config with etemenanki-app --test -c config.toml before you start it. --test builds every inbound and reads its certificate and key files, but it binds nothing (see what --test cannot catch).
Common fields
Section titled “Common fields”These keys sit directly in each [[inbound]] table. Unknown keys are errors, so a misspelt listen stops the config instead of silently falling back to the default.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
tag | string | yes | — | Name of the inbound. It must be unique among inbounds, otherwise the config fails with duplicate inbound tag. Routing rules match it with inbound_tag, and log lines use it. Inbound and outbound tags are separate namespaces. |
protocol | string (enum) | yes | — | The protocol clients speak: socks, http, trojan, vless, vmess, shadowsocks, hysteria2 (aliases hysteria and hy2) or tun. Values are case-sensitive. wireguard is refused with wireguard cannot be used as an inbound (no server implementation); anything else fails with unknown protocol. |
listen | string | no | "127.0.0.1" | Address to bind. Defaults to loopback on purpose; write 0.0.0.0 or :: to accept connections from the network. Write IPv6 addresses bare (::, not [::]). A host name is resolved when the listener binds. A value starting with / is a Unix socket path; a value starting with @ (abstract socket) is refused. Must be absent for tun. |
port | u16 | depends | — | Port to bind, from 0 to 65535. Required when listen is absent, an IP address or a host name (port is required). Must be absent for a Unix socket (a unix socket listen has no port; remove port) and for tun. 0 is accepted and lets the kernel pick a free port. For hysteria2 this is a UDP port. |
stream | table | no | — | Transport under the protocol: network (tcp, tls, ws, grpc), security and the tls, ws and grpc subtables. Only http, trojan, vless and vmess on an IP listener use it. In every other case a network other than tcp or a security other than none is refused. |
address_family | string | no | — | Accepted so that inbound and outbound tables share a shape, but it has no effect on an inbound. The value is not validated. |
sniffing | bool | no | true | Read a TLS SNI or HTTP Host from the first bytes of a flow whose destination is a bare IP, so that domain rules can match it. Flows addressed by domain are never inspected. |
settings | table | no | {} | Protocol-specific settings, described on each protocol page. Unknown keys are refused with inbound TAG: invalid settings: .... These errors carry no line number. |
A misspelt key is reported with its line:
configuration invalid: TOML parse error at line 4, column 1 |4 | lisen = "0.0.0.0" | ^^^^^unknown field `lisen`, expected one of `tag`, `protocol`, `listen`, `port`, `stream`, `address_family`, `sniffing`, `settings`Protocols at a glance
Section titled “Protocols at a glance”protocol |
Listens on | [inbound.stream] |
UDP | mux.cool / XUDP | Client authentication | Unix socket |
|---|---|---|---|---|---|---|
socks |
TCP | No | SOCKS5 UDP ASSOCIATE, on by default (udp = true) |
No | None, or username and password | Yes, with udp_bind or udp = false |
http |
TCP | tcp, tls, ws, grpc |
No | No | Basic auth when accounts is set, otherwise open |
Yes |
trojan |
TCP | tcp, tls, ws, grpc |
Yes | Yes | Password per user | Yes |
vless |
TCP | tcp, tls, ws, grpc |
Yes | Yes | UUID per user | Yes |
vmess |
TCP | tcp, tls, ws, grpc |
Yes | Yes | UUID per user | Yes |
shadowsocks |
TCP | No | No, TCP only | No | Cipher and password, optionally one password per user | Yes |
hysteria2, hysteria, hy2 |
UDP (QUIC) | No | When udp = true (off by default) |
No; every circuit is its own QUIC stream | One shared password, or a user name and password per user | No |
tun |
A network interface | No | When udp = true (on by default) |
No | None; the device is local | Not applicable |
Notes on the table:
- Listens on is what etemenanki-app binds.
hysteria2binds a UDP socket, so it can share a port number with a TCP inbound, for example Trojan over TLS and Hysteria 2 both on port 443. - mux.cool / XUDP is the server side of Xray’s multiplexing. Trojan, VLESS and VMess inbounds accept it automatically; there is no key to turn it on or off. One carrier connection holds at most 256 sub-flows.
- UDP means the inbound relays datagrams, not just streams. HTTP and Shadowsocks inbounds carry TCP only.
wireguardexists only as an outbound; see WireGuard. As an inbound it is refused withwireguard cannot be used as an inbound (no server implementation).
How listen and port pick a listener
Section titled “How listen and port pick a listener”etemenanki-app decides what to bind while it builds the config, so --test refuses the same shapes a start would.
flowchart TB
A["[[inbound]]"] --> B{"protocol = tun?"}
B -- yes --> T["TUN device: listen and port must be absent"]
B -- no --> C{"listen starts with / ?"}
C -- yes --> U["Unix socket: port must be absent"]
C -- no --> D{"listen starts with @ ?"}
D -- yes --> E["error: abstract sockets not supported"]
D -- no --> F{"port set?"}
F -- no --> G["error: port is required"]
F -- yes --> H{"protocol"}
H -- "hysteria2, hysteria, hy2" --> UDP["UDP socket on listen:port"]
H -- "socks, http, trojan, vless, vmess, shadowsocks" --> TCP["TCP listener on listen:port"]
H -- "anything else" --> X["error: unknown protocol, or wireguard refused"]
Apart from the tun check, the listen and port checks run before the protocol is looked at. An inbound with an unknown protocol and no port therefore reports port is required, not unknown protocol.
Loopback by default
Section titled “Loopback by default”When listen is absent, the inbound binds 127.0.0.1. This is deliberate: a proxy that was meant to stay local must never end up on the network because of a typo or a wrongly nested key. A server that should accept connections from other machines says so explicitly:
listen = "0.0.0.0"port = 443listen = "::"port = 443On Linux with the default net.ipv6.bindv6only = 0, a socket bound to :: also accepts IPv4 connections.
listen = "192.0.2.10"port = 443Addresses, host names and ports
Section titled “Addresses, host names and ports”- Write IPv6 addresses bare:
"::"or"2001:db8::10". A bracketed"[::]"passes--testbut fails when the listener binds:failed to lookup address information: Name or service not known. - A host name such as
"localhost"is resolved when the listener binds. If it resolves to several addresses, the first one that binds is used, and only that one. portis a 16-bit number.70000is a parse error:invalid value: integer `70000`, expected u16.port = 0lets the kernel choose a free port. The startup log prints the configured value (127.0.0.1:0), not the port the kernel chose, so the log does not tell you which port is in use.- Ports below 1024 need root or the
CAP_NET_BIND_SERVICEcapability. That is enforced by the kernel when the listener binds.
TUN has no listener
Section titled “TUN has no listener”A tun inbound creates a network interface instead of binding a socket. It refuses both keys with tun owns a network interface and has no listener; remove listen/port. The device’s name, addresses and routes are in its [inbound.settings]; see TUN.
Unix-socket listeners
Section titled “Unix-socket listeners”A listen value that starts with / makes the inbound listen on a Unix-domain socket at that path instead of on TCP. Use it when a local program, or a web server on the same host, talks to the proxy without going through the network stack.
[[inbound]]tag = "socks-unix"protocol = "socks"listen = "/run/etemenanki/socks.sock"
[inbound.settings]udp = trueudp_bind = "127.0.0.1" # a Unix socket has no local IP for the UDP relayWhat a Unix socket accepts
Section titled “What a Unix socket accepts”| Rule | Error when broken |
|---|---|
The path is absolute. A relative path such as run/x.sock is read as a host name. |
inbound TAG: port is required |
No port. |
inbound TAG: a unix socket listen has no port; remove port |
No abstract sockets (@name). |
inbound TAG: abstract unix sockets are not supported; use a filesystem path |
No transport: in [inbound.stream], network may only be tcp and security only none. |
inbound TAG: protocol vless over a unix socket does not support stream network "ws" (or stream security "tls") |
Not hysteria2, which needs UDP. |
inbound TAG: hysteria2 listens on UDP and cannot use a unix socket |
socks with udp = true (the default) also sets udp_bind, or turns UDP off. |
inbound TAG: socks over a unix socket has no local IP for UDP associate; set udp_bind or udp = false |
Every other stream protocol works on a Unix socket: socks, http, trojan, vless, vmess and shadowsocks. The protocol runs directly on the socket, with no TLS, WebSocket or gRPC layer, because there is nothing on a local socket to disguise it from.
Socket file lifecycle
Section titled “Socket file lifecycle”- At every bind, at start or on a reload, if a socket file already exists at the path, etemenanki-app assumes it is stale, left by a crashed run or by the previous generation, removes it and binds a new one.
- Any other file at the path is left alone and the bind fails with
PATH exists and is not a socket. - The parent directory must exist. etemenanki-app does not create it; a missing directory fails with
No such file or directory (os error 2). - The path length is limited by the operating system (about 107 bytes on Linux). A longer path fails with
path must be shorter than SUN_LEN. - On shutdown (
SIGINTorSIGTERM) and on every reload, the inbound removes its socket file when it stops listening. The next generation then binds a fresh one. - Permissions come from the process’s umask. etemenanki-app does not change the socket’s mode or owner, so control who may connect with the permissions of the directory that holds it.
No client address
Section titled “No client address”A connection over a Unix socket has no client IP address. Routing rules that match source_cidr never match flows from a Unix-socket inbound, and a SOCKS inbound has no local IP on which to place its UDP relay, which is why it needs udp_bind.
A SOCKS UDP relay accepts datagrams from one client only. Over TCP, that client is the IP address the control connection came from. A Unix socket has no such address, so the client has to name its source itself: in its UDP ASSOCIATE request, it must give the exact IP address and port its datagrams will come from, and the address must be in a family that udp_bind hears: IPv4 for an IPv4 or IPv4-mapped udp_bind (such as ::ffff:192.0.2.10), IPv6 for any other IPv6 one, and either for ::. With udp_bind = "127.0.0.1", for example, that means an IPv4 address such as 127.0.0.1:5000. The relay then accepts datagrams from that address and port only.
The inbound refuses a request that leaves the address or the port unspecified or zero, names a domain instead of an IP address, or names an address in a family the relay cannot receive. It answers with SOCKS5 reply code 0x02 (“connection not allowed by ruleset”) and closes the control connection. RFC 1928 lets a client send all zeros when it does not know its source address, and many clients do. Such a client can use CONNECT over a Unix socket, but not UDP ASSOCIATE. --test cannot catch this, and the table above does not list it, because the inbound refuses the request at run time and the config itself is valid. The SOCKS page covers the UDP relay in full.
Transports: [inbound.stream]
Section titled “Transports: [inbound.stream]”[inbound.stream] puts a transport under the protocol: TLS, WebSocket or gRPC, optionally with TLS under WebSocket or gRPC. Four protocols use it, and only on an IP listener: http, trojan, vless and vmess.
[inbound.stream]network = "grpc"security = "tls"
[inbound.stream.grpc]service_name = "example"
[inbound.stream.tls]cert_file = "/etc/etemenanki/cert.pem"key_file = "/etc/etemenanki/key.pem"Every other inbound refuses a transport it cannot run instead of silently dropping it, so an operator does not end up with a bare port while believing it is disguised. For socks, shadowsocks, hysteria2 and tun, and for any inbound on a Unix socket, a network other than tcp or a security other than none is an error:
inbound ss: protocol shadowsocks does not support stream security "tls"hysteria2 carries its own TLS: its certificate and key go in [inbound.settings], not in [inbound.stream.tls]. Every network and security value, the ws, grpc and tls keys, and the errors they produce are on the transports page.
Sniffing
Section titled “Sniffing”With sniffing = true, the default, an inbound reads a domain name out of the opening bytes of a flow whose destination is a bare IP address: the SNI of a TLS ClientHello, or the Host header of an HTTP request. Routing then matches domain and geosite rules against that name as well as the address. The destination the flow is dialled to does not change.
- Only flows addressed by IP are inspected. A flow that already names a domain is routed on that name immediately.
- The inbound waits at most 300 milliseconds and reads at most 4 KiB. A client that sends nothing first, as in SSH or SMTP, is routed on its IP once the wait expires.
- Malformed or unrecognised bytes never fail a connection; the flow is routed on its address.
- A mux.cool sub-flow is sniffed only from the data that arrived with its opening frame, so one sub-flow never holds up the others on the same carrier.
For SOCKS, HTTP CONNECT and Hysteria 2, sniffing changes when the client gets its answer. Normally the inbound dials the outbound first and replies “connected” only once the dial succeeds, or replies with a refusal. When it has to sniff a request addressed by IP, it must reply first, because the client sends no data until it has the reply. Such a client then learns of an unreachable target when the connection closes, not from the reply.
Set sniffing = false on an inbound whose clients always send domains, or whose traffic you route only by IP, port or inbound tag. The routing page explains how sniffed names meet the rules.
Limits
Section titled “Limits”Every inbound that accepts sockets (all protocols except hysteria2 and tun, on TCP or on a Unix socket) has these fixed limits. They are not configurable.
| Limit | Value | What happens at the limit |
|---|---|---|
| Live connections per inbound | 65,536 | A new connection is closed as soon as it is accepted. A connection counts from accept until it closes, however many streams a gRPC connection carries. |
| Concurrent handshakes per inbound | 2,048 | A new connection is closed as soon as it is accepted. |
| Handshake time | 10 seconds | A client that has not completed its protocol request in time is disconnected. The transport step before it (TLS, the WebSocket upgrade, the gRPC connection preface) has its own 10 seconds. |
| mux.cool sub-flows per carrier connection | 256 | A further sub-flow is declined; the carrier and its other sub-flows carry on. |
| Sniffing wait / bytes | 300 ms / 4 KiB | The flow is routed on its address. |
Connections dropped at a limit are logged at debug level only. The limits of the other two inbound types are settings:
| Inbound | Setting | Default |
|---|---|---|
hysteria2 |
max_connections (QUIC connections) |
4096 |
hysteria2 |
max_circuits (live circuits across the listener) |
65,536 |
tun |
max_flows (live flows across the device) |
65,536 |
Every limit in etemenanki-app is collected on the limits page.
Reloads and bind failures
Section titled “Reloads and bind failures”When the config file changes, etemenanki-app builds the new config completely before it touches the running one. If the new config is invalid, the old generation keeps serving. If it is valid, every inbound of the old generation stops, including the ones whose settings did not change, and every connection on them is dropped. The new generation then binds its listeners.
Binding behaves differently at start and on reload:
- At start, the first inbound that fails to bind stops the process:
failed to start: inbound TAG bind 127.0.0.1:1080 failed: Address already in use (os error 98). - On a reload, a listener that fails to bind is logged as
inbound TAG bind ADDRESS failed: ...and stays down, while the other inbounds start. etemenanki-app tries it again only on the next reload, which needs a change to the config file’s contents.
The hot reload page covers the sequence in detail.
What --test cannot catch
Section titled “What --test cannot catch”--test builds every inbound and reads every certificate, but it binds nothing. These problems only appear when the listener binds, at start or on a reload:
- the port is already in use, including by another inbound in the same file;
- a bracketed IPv6 address such as
"[::]", or a host name that does not resolve; - a port below 1024 without the privilege to bind it;
- a Unix-socket path whose directory is missing, that is too long, or where a file that is not a socket already exists;
- creating a
tundevice, which needs the privileges to do so.
Common errors
Section titled “Common errors”Build errors are printed as configuration invalid: ... by --test and as failed to start: ... at start. Most carry the inbound’s tag.
| Error | Cause | Fix |
|---|---|---|
inbound TAG: port is required |
An IP listen, or a missing or relative listen, without port. |
Add port, or give an absolute socket path. |
inbound TAG: unknown protocol "SOCKS" |
Protocol names are lowercase and case-sensitive. | Use one of the values in the table above. |
wireguard cannot be used as an inbound (no server implementation) |
protocol = "wireguard" in an [[inbound]]. |
Use WireGuard as an outbound only. |
duplicate inbound tag: TAG |
Two inbounds share a tag. | Rename one. |
inbound TAG: a unix socket listen has no port; remove port |
A socket path together with port. |
Remove port. |
inbound TAG: tun owns a network interface and has no listener; remove listen/port |
listen or port on a tun inbound. |
Remove both. |
inbound TAG: protocol socks does not support stream network "ws" |
A transport on an inbound that cannot carry one. | Remove [inbound.stream], or switch to vless, vmess, trojan or http. |
inbound TAG: tls stream needs tls.cert_file |
TLS requested without a certificate. | Set cert_file and key_file under [inbound.stream.tls]. |
inbound TAG: invalid settings: unknown field ... |
A misspelt key in [inbound.settings]. These errors have no line number. |
Compare against the protocol page. |
inbound TAG bind unix:PATH failed: PATH exists and is not a socket |
A file that is not a socket sits at the Unix-socket path. | Move the file, or choose another path. |
The gotchas page lists more surprises that are not errors.