Outbounds
An outbound is where a connection goes after an inbound has accepted it. It can dial the destination directly (freedom), throw the connection away (blackhole), or hand it to another proxy server as a client of SOCKS, HTTP, Trojan, VLESS, VMess, Shadowsocks, Hysteria 2 or WireGuard. The router picks one outbound, or a balancer made of several, for every TCP connection and for every UDP packet.
This page covers what all outbounds have in common: the [[outbound]] fields, the address_family policy, which protocols carry UDP, and how UDP is routed. The settings of each protocol are on its own page.
A minimal example
Section titled “A minimal example”A configuration needs at least one outbound. With no [route] section, every flow goes to the first one:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "direct"protocol = "freedom"A more typical client sends most traffic through a proxy server, some straight out, and blocks some:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
# The first outbound is the default unless [route].default names another.[[outbound]]tag = "proxy"protocol = "vless"server = "proxy.example.com"port = 443address_family = "prefer_ipv4"
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ws"
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"
[[outbound]]tag = "direct"protocol = "freedom"address_family = "ipv4_only"
[[outbound]]tag = "block"protocol = "blackhole"
[[route.rule]]outbound = "block"domain_suffix = ["ads.example.com"]
[[route.rule]]outbound = "direct"cidr = ["10.0.0.0/8", "192.168.0.0/16"]proxy is the default because it comes first. The VLESS client reaches proxy.example.com over WebSocket inside TLS, and presents proxy.example.com as the TLS server name because server is the fallback when tls.server_name is not set.
Common fields
Section titled “Common fields”Every [[outbound]] table accepts exactly these keys. Any other key is a parse error that names the line, for example unknown field `sever`, expected one of `tag`, `protocol`, `server`, `port`, `stream`, `address_family`, `settings` .
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
tag | string | yes | — | Name of the outbound. Route rules, [route].default and [[balancer]].outbounds refer to it. Must be unique among outbounds (duplicate outbound tag: …), and no [[balancer]] may reuse it. Without [route].default, the first [[outbound]] in the file is the default route. |
protocol | string (enum) | yes | — | What the outbound speaks: freedom (alias direct), blackhole (alias block), socks, http, trojan, vless, vmess, shadowsocks, hysteria2 (aliases hysteria, hy2) or wireguard. Matched exactly and case-sensitively; anything else fails with unknown protocol. |
server | string | depends | — | Host name or IP address of the upstream server, without a port. Required by socks, http, trojan, vless, vmess, shadowsocks and hysteria2 (missing server); never used for traffic by freedom, blackhole and wireguard, where it only matters as a balancer health-probe target. Write an IPv6 address bare (2001:db8::1), without brackets. It is also the fallback for the TLS server name (including hysteria2 server_name), the WebSocket Host and the gRPC authority. |
port | u16 | depends | — | Port of the upstream server. Required by the same protocols as server (missing port); the others use it only as a balancer health-probe target. For hysteria2 it is a UDP port; for every other protocol, a TCP port. |
stream | table | no | — | Transport to the upstream: network (tcp, tls, ws, grpc), security and the tls, ws and grpc sub-tables. Honoured by socks, http, trojan, vless, vmess and shadowsocks. freedom, blackhole, wireguard and hysteria2 refuse any network other than tcp and any security other than none, and ignore the tls, ws and grpc sub-tables. Absent means plain TCP. |
address_family | string (enum) | no | "auto" | Which IP family to use when a name is resolved: auto, ipv4_only, ipv6_only, prefer_ipv4 or prefer_ipv6, plus aliases. Trimmed, case-insensitive, and - counts as _. For a proxy outbound it applies to the server name; for freedom, to the destination; for wireguard, to destinations inside the tunnel. An unknown value fails with invalid address_family. |
settings | table | depends | {} | Protocol-specific settings, described on each protocol page. Required for trojan, vless, vmess, shadowsocks, hysteria2 and wireguard, which have mandatory keys; optional for socks and http; never read by freedom and blackhole. Unknown keys are refused, and errors here read outbound <tag>: invalid settings: …. |
protocol
Section titled “protocol”| Value | Aliases | What it does | Page |
|---|---|---|---|
freedom |
direct |
Dials the destination itself | Freedom and blackhole |
blackhole |
block |
Drops the flow | Freedom and blackhole |
socks |
SOCKS5 client: CONNECT, and UDP ASSOCIATE for UDP |
SOCKS | |
http |
HTTP CONNECT client |
HTTP | |
trojan |
Trojan client | Trojan | |
vless |
VLESS client | VLESS | |
vmess |
VMess client | VMess | |
shadowsocks |
Shadowsocks client, AEAD or 2022 (chosen by settings.method) |
Shadowsocks | |
hysteria2 |
hysteria, hy2 |
Hysteria 2 client over QUIC | Hysteria 2 |
wireguard |
A userspace WireGuard tunnel to one peer | WireGuard |
Protocol names are case-sensitive: protocol = "Freedom" fails with outbound direct: unknown protocol "Freedom".
server and port
Section titled “server and port”The six stream proxies and Hysteria 2 dial an upstream server, so they need both keys. The other three never read them.
freedomgoes wherever each flow is addressed.blackholegoes nowhere.wireguardtakes its peer fromsettings.endpointashost:port.
Anything written in server or port for these three is accepted and never used for traffic. The one place it still counts is a balancer: see Balancer member below.
server holds a host name or an IP address and nothing else. Write IPv6 addresses bare:
server = "2001:db8::10" # correct# server = "[2001:db8::10]" # passes --test, then fails every dialA value that does not parse as an IP address is treated as a host name. [2001:db8::10] with brackets is therefore a “name” that no resolver can answer. --test does not resolve names, so it cannot catch this; the connections fail at run time.
stream
Section titled “stream”[outbound.stream] chooses the transport a proxy client uses to reach its server: plain TCP, TLS, WebSocket or gRPC, with or without TLS underneath. The keys are the same as on inbounds and are described on Transports.
Four protocols have no use for a transport:
freedomandblackholedo not dial a proxy server.wireguardsends its own UDP packets.hysteria2runs its own QUIC connection; its TLS settings live in[outbound.settings]instead.
For these four, a network other than tcp or a security other than none is refused rather than silently dropped:
outbound wg: protocol wireguard does not support stream network "ws"outbound wg: protocol wireguard does not support stream security "tls"settings
Section titled “settings”[outbound.settings] holds what only one protocol understands: a password, a UUID, a cipher, WireGuard keys. Each protocol page lists its keys. Two things hold for all of them:
- Unknown keys are refused. A typo such as
usernamein a SOCKS outbound fails withoutbound x: invalid settings: unknown field `username`, expected `user` or `pass`. - The settings table is read after the file has been parsed, so its errors carry the outbound’s tag instead of a line number.
freedom and blackhole take no settings and never read the table, so nothing in it is checked.
What each protocol supports
Section titled “What each protocol supports”| Protocol | Needs server and port |
[outbound.stream] |
Carries UDP | Balancer member |
|---|---|---|---|---|
freedom |
no (ignored) | refused | yes | only with server and port |
blackhole |
no (ignored) | refused | swallowed | only with server and port |
socks |
yes | yes | yes, by UDP ASSOCIATE |
yes |
http |
yes | yes | no | yes |
trojan |
yes | yes | yes | yes |
vless |
yes | yes | yes, one destination per association | yes |
vmess |
yes | yes | yes, one destination per association | yes |
shadowsocks |
yes | yes | no | yes |
hysteria2 |
yes (UDP port) | refused | yes, if the server allows UDP | no |
wireguard |
no (settings.endpoint) |
refused | yes | only with server and port |
[outbound.stream]“refused” means anetworkorsecuritythat asks for anything but plain TCP is an error, as described above.- Carries UDP says what happens to a UDP packet routed to the outbound.
blackholeaccepts and discards it.httpandshadowsocks(both AEAD and 2022 methods) have no datagram support in this client, so packets routed to them are dropped (see UDP to an outbound without datagrams).vlessandvmessfix the destination of a UDP association in its header, and their UDP framing carries no per-packet address. Every packet on one association’s sub-link goes to the destination of the packet that opened it (see How UDP reaches an outbound).socks,trojan,hysteria2,wireguardandfreedomaddress each packet on its own.hysteria2relays UDP only if the server allows it. When the server refuses, the app logshysteria2: …; datagrams routed to this outbound are droppedonce per outbound, atwarnlevel, and drops those packets.
- Balancer member is decided by the balancer’s health probe, which is a TCP connect to the member’s
serverandport.hysteria2is always refused: its server listens on UDP only, so a TCP probe would mark it down forever.freedom,blackholeandwireguardare refused when they have noserverandport, which is the normal case. The error for all of these isbalancer <tag>: outbound <tag> has no upstream a TCP health probe can reach, so it cannot be balanced. If you do writeserverandporton one of these three, it is accepted as a member, and the probe checks that address although the outbound never sends traffic there. See Balancers.
address_family
Section titled “address_family”address_family decides which of a name’s IP addresses an outbound may use, and in which order it tries them. It is set per outbound, so a direct outbound can stay on IPv4 while a proxy outbound prefers IPv6.
Accepted values
Section titled “Accepted values”The value is trimmed, lower-cased, and every - is turned into _ before it is matched. "IPv4-Only", " ipv4_only " and "ipv4only" all mean the same thing. The value must be a TOML string: address_family = 4 is a type error, address_family = "4" is fine.
| Policy | Also accepted | Addresses used | Order |
|---|---|---|---|
auto (default) |
"" (empty string) |
IPv4 and IPv6 | as the resolver returned them |
ipv4_only |
ipv4, v4, 4, ipv4only |
IPv4 only | as returned |
ipv6_only |
ipv6, v6, 6, ipv6only |
IPv6 only | as returned |
prefer_ipv4 |
prefer_v4, ipv4_prefer, v4_prefer |
IPv4 and IPv6 | IPv4 first, IPv6 kept as a fallback |
prefer_ipv6 |
prefer_v6, ipv6_prefer, v6_prefer |
IPv4 and IPv6 | IPv6 first, IPv4 kept as a fallback |
Anything else stops the configuration from loading:
outbound direct: invalid address_family "either"auto keeps the resolver’s order. With the default host resolver, that is the order the system returns, which follows the host’s address-selection rules. With a DNS server configured in [dns], IPv4 answers come before IPv6 ones. Choose an explicit policy only when that answer is wrong for one outbound, for example on a host whose IPv6 route is broken.
For a TCP connection, the outbound tries the allowed addresses one after another in that order and uses the first one that connects. Hysteria 2 does the same with its QUIC connection to server. Every lookup goes through the resolver configured in [dns] (the host resolver unless [dns] says otherwise), with one exception noted below.
What it applies to
Section titled “What it applies to”The same key resolves a different name depending on the protocol:
| Protocol | address_family applies to |
|---|---|
freedom |
The destination of each flow. For UDP it also limits which local sockets are opened: ipv4_only opens only an IPv4 socket, ipv6_only only an IPv6 one. |
socks, http, trojan, vless, vmess, shadowsocks |
The server name. The destination is passed to the upstream proxy as it arrived, domain names included, and the upstream resolves it. |
hysteria2 |
The server name. As with the proxies above, the destination goes to the server unresolved. |
wireguard |
Destinations inside the tunnel. A name is resolved with the policy and then limited to the families that have an address in settings.address. The peer endpoint is not affected (see below). |
blackhole |
Nothing. The value is not even validated. |
A destination that is already an IP address is not resolved. What happens to it depends on the traffic:
- TCP through
freedomorwireguard: the address is filtered like a resolved one. Afreedomoutbound withaddress_family = "ipv4_only"refuses a connection to2001:db8::1. - UDP through
freedom: the policy does not filter IP addresses, butipv4_onlyopens only an IPv4 socket andipv6_onlyonly an IPv6 one. A packet to an address of the other family is dropped, and the association carries on. - UDP through
wireguard: a packet to an IP address is passed into the tunnel as it is; neither the policy norsettings.addressis checked.
When no address is left
Section titled “When no address is left”If the policy filters out every address, the connection fails with a message that names the policy and the target, for example:
dial: no usable ipv4_only destination address for example.com:443A wireguard outbound uses the prefix wireguard: instead of dial:, and adds which families the tunnel addresses allow, for example wireguard: no usable ipv6_only destination address for example.com:443 (local address supports IPv4 only).
This happens at run time, per connection, not at load time. The protocol handlers log failed connections at debug level, so set [log].level = "debug" to see these messages.
For UDP through freedom or wireguard, a domain is resolved once per sub-link (see How UDP reaches an outbound), with the policy applied, and the first allowed address is used for every later packet to that name. Names are looked up one at a time, and packets to other names wait meanwhile. A name that does not resolve, or resolves to nothing allowed, has its packets dropped for as long as that sub-link lives.
How UDP reaches an outbound
Section titled “How UDP reaches an outbound”A TCP connection is routed once, when it opens, and stays on the outbound the router picked. A UDP association cannot work that way: each datagram carries its own destination, and one association, such as a SOCKS5 UDP ASSOCIATE, may talk to many peers.
etemenanki-app therefore routes UDP packet by packet. Each association keeps its own set of sub-links, at most one per outbound:
flowchart LR
C["Client UDP association"] --> R{"Route each packet"}
R -- "to 192.0.2.53:53" --> D["sub-link: direct"]
R -- "to example.com:443" --> P["sub-link: proxy"]
R -- "to ads.example.com:443" --> B["sub-link: block"]
D --> R2["Replies merged back"]
P --> R2
B -.-> R2
R2 --> C
What this means in practice:
- Rules see every packet. The router matches each datagram on its own destination and port, together with the association’s inbound tag and source address and
network = "udp". No sniffed domain is attached to a UDP packet, so a packet addressed to an IP address matches only IP, port and other non-domain rules. - One association can use several outbounds at once. DNS can go direct while a game’s traffic goes through a proxy, without the client doing anything special.
- Destinations share a sub-link per outbound. All packets routed to the same outbound use the same sub-link, however many different destinations they have. The first packet to an outbound opens the sub-link.
- VLESS and VMess sub-links keep their first destination. These protocols name one destination when the UDP association opens and carry no address per packet. Every later packet that the router sends to the same
vlessorvmessoutbound goes to that first destination, whatever its own address, and replies appear to come from it. If one client association talks to several peers through VLESS or VMess, only the first peer is reached correctly. - At most 64 sub-links per association. When a 65th outbound is needed, the association closes the sub-link it sent to least recently. Replies still in flight on that sub-link are lost. You only reach this with more than 64 distinct outbounds and balancers in one association.
- A packet whose sub-link cannot be opened is dropped. The association carries on, and the next packet for that outbound tries again. While a sub-link is being opened, the association’s outgoing packets wait until it opens or fails.
- A sub-link that ends is replaced. If a sub-link’s connection closes, for example because the upstream proxy dropped it, the association removes it, and the next packet for that outbound opens a new one.
- A balancer picks its member when its sub-link opens. The association then keeps using that member until the sub-link ends.
UDP to an outbound without datagrams
Section titled “UDP to an outbound without datagrams”http and shadowsocks outbounds carry only TCP. The router does not know that: a UDP packet that matches a rule pointing at one of them is routed there, the sub-link fails to open, and the packet is dropped. The app logs this at debug level:
udp fan-out: opening an outbound failed: http carries no datagramsFor Shadowsocks the message names shadowsocks or shadowsocks-2022. Every later packet for that outbound is dropped the same way.
To keep UDP working, send it somewhere else before the rule that catches it. Because rules are first-match, a network = "udp" rule placed first does this:
# UDP goes direct; everything else falls through to the HTTP proxy.[[route.rule]]outbound = "direct"network = "udp"
[[route.rule]]outbound = "corp-http"domain_suffix = ["example.com"]Tags, the default route and balancers
Section titled “Tags, the default route and balancers”- Tags must be unique. Two outbounds with the same tag fail with
duplicate outbound tag: <tag>. - The first outbound is the default. Flows that match no rule go to
[route].defaultif it is set, and to the first[[outbound]]in the file otherwise. Put the outbound you want as the fallback first, or setdefaultexplicitly. See Routing. - A balancer tag works wherever an outbound tag does, in
[route].defaultand in[[route.rule]].outbound. A balancer tag must not repeat an outbound tag (balancer tag <tag> collides with an outbound tag). A balancer cannot list another balancer as a member: the member is reported asbalancer <tag> references unknown outbound tag: <tag>. - Unknown tags are errors. A rule or default that names no outbound or balancer fails with
route references unknown outbound tag: <tag>.
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "trojan-a"protocol = "trojan"server = "2001:db8::10"port = 443
[outbound.stream]network = "tls"
[outbound.stream.tls]server_name = "proxy.example.com"
[outbound.settings]password = "replace-with-a-long-random-password"
[[outbound]]tag = "ss-b"protocol = "shadowsocks"server = "203.0.113.20"port = 8388
[outbound.settings]method = "2022-blake3-aes-256-gcm"password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # generate with: openssl rand -base64 32
[[balancer]]tag = "pool"outbounds = ["trojan-a", "ss-b"]
[[route.rule]]outbound = "pool"domain_suffix = ["example.com"]Here direct stays the default because it comes first. Traffic for example.com goes to trojan-a while its health probe succeeds and to ss-b when it does not, because the default balancer strategy, failover, picks the first healthy member in list order. ss-b is a Shadowsocks outbound, so UDP for example.com is dropped whenever the balancer picks it. Add a network = "udp" rule ahead of this one if that matters.
Common errors
Section titled “Common errors”Errors in this table stop etemenanki-app --test -c config.toml and a normal start. Under --test the logged line starts with configuration invalid:; at start-up it starts with failed to start:.
| Message | Cause | Fix |
|---|---|---|
config defines no outbounds |
The file has no [[outbound]] |
Add at least one, even if it is only freedom |
duplicate outbound tag: <tag> |
Two outbounds share a tag | Rename one |
outbound <tag>: unknown protocol "…" |
A misspelt or capitalised protocol | Use one of the lowercase names in the protocol table |
outbound <tag>: missing server / missing port |
A proxy outbound without an upstream | Add server and port |
outbound <tag>: invalid address_family "…" |
A value outside the accepted list | Use auto, ipv4_only, ipv6_only, prefer_ipv4 or prefer_ipv6 |
outbound <tag>: protocol <protocol> does not support stream network "…" / stream security "…" |
[outbound.stream] on freedom, blackhole, wireguard or hysteria2 |
Remove the [outbound.stream] block |
outbound <tag>: invalid settings: missing field … / unknown field … |
A required setting is absent, or a key is misspelt | Check the protocol’s page for the exact keys |
outbound <tag>: wireguard address_family ipv6_only needs an IPv6 address |
The policy excludes every tunnel address | Add a tunnel address of that family, or change the policy |
balancer <tag>: outbound <tag> has no upstream a TCP health probe can reach, so it cannot be balanced |
A hysteria2 member, or a freedom, blackhole or wireguard member without server and port |
Balance only stream proxies |
balancer <tag> references unknown outbound tag: <tag> |
A member that is not an outbound, including another balancer | Name outbounds only |
balancer tag <tag> collides with an outbound tag |
A balancer reuses an outbound’s tag, or an earlier balancer’s | Rename the balancer |
route references unknown outbound tag: <tag> |
A rule or default names a tag that does not exist | Fix the tag, or define the outbound |
These appear only while traffic flows, in the debug log:
| Message | Cause |
|---|---|
dial: no usable <policy> destination address for <host>:<port> |
address_family filtered out every address of the destination or server (wireguard: instead of dial: for WireGuard) |
udp fan-out: opening an outbound failed: … carries no datagrams |
UDP was routed to http or shadowsocks |