SOCKS
SOCKS is a protocol that local applications commonly use to reach a proxy. Browsers, curl, git, package managers and most desktop clients can point at a SOCKS proxy without extra software. etemenanki-app implements it in both directions:
- as an inbound (
protocol = "socks"under[[inbound]]), it serves SOCKS4, SOCKS4a and SOCKS5 clients on one port, with optional username/password authentication and SOCKS5 UDP; - as an outbound (
protocol = "socks"under[[outbound]]), it is a SOCKS5 client that forwards flows to an upstream SOCKS5 server, TCP and UDP.
Use the inbound as the entry point on a workstation or inside a trusted network, and the outbound to chain through an existing SOCKS5 proxy. SOCKS encrypts nothing, so it is not the protocol to expose across the internet; see Security.
At a glance
Section titled “At a glance”| Inbound | Outbound | |
|---|---|---|
| Versions | SOCKS4, SOCKS4a, SOCKS5 on the same port | SOCKS5 only |
| Authentication | none, or SOCKS5 username/password (RFC 1929) | none, or SOCKS5 username/password |
| TCP | CONNECT |
CONNECT |
| UDP | UDP ASSOCIATE, on by default |
UDP ASSOCIATE |
BIND |
refused | not used |
Transport ([.stream]) |
plain TCP only | TCP, TLS, WebSocket or gRPC |
| Unix socket listener | yes | not applicable |
A working example
Section titled “A working example”This config accepts password-authenticated SOCKS5 clients on every IPv4 address (listen = "0.0.0.0"), including UDP, and sends everything out directly through a freedom outbound.
# A password-protected SOCKS5 inbound on every IPv4 address, with UDP ASSOCIATE,# relaying everything directly to the internet.## SOCKS carries no encryption: credentials and traffic cross the network in# the clear. Expose this only on a network you trust or behind a tunnel.
[log]level = "info"
[[inbound]]tag = "socks-in"protocol = "socks"listen = "0.0.0.0"port = 1080
[inbound.settings]auth = "password"accounts = [ { user = "alice", pass = "replace-with-a-long-random-password" }, { user = "bob", pass = "replace-with-another-long-random-password" },]# UDP ASSOCIATE is on by default. Each association opens a relay socket on a# UDP port the OS picks, on the address the client connected to; set udp_bind# to pin that address instead.udp = true
[[outbound]]tag = "direct"protocol = "freedom"
[route]default = "direct"Check it before you start it:
etemenanki-app --test -c socks-auth.toml--test prints Configuration OK. or the first error. Then start the proxy and try it from a client. The address in the proxy URL decides who resolves the destination name:
# socks5h:// sends the hostname to the proxy, which resolves it.curl -x 'socks5h://alice:replace-with-a-long-random-password@192.0.2.10:1080' https://example.com/# socks5:// resolves locally and sends an IP address. With sniffing on,# the proxy answers at once and reads the TLS server name from the first bytes.curl -x 'socks5://alice:replace-with-a-long-random-password@192.0.2.10:1080' https://example.com/# Only against an inbound with auth = "none": SOCKS4 has no password method.curl -x 'socks4a://127.0.0.1:1080' https://example.com/If the username or password contains characters that are special in a URL, such as @ or /, pass the credentials with --proxy-user 'alice:…' instead of inside the URL. curl only exercises TCP; to test UDP, use a client that implements UDP ASSOCIATE.
Inbound settings
Section titled “Inbound settings”The inbound uses the common [[inbound]] keys (tag, listen, port, sniffing), described on Inbounds. Its own keys go in [inbound.settings]:
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
auth | string (enum) | no | "none" | How clients authenticate. "none" accepts anyone and allows SOCKS4, SOCKS4a and SOCKS5. "password" requires SOCKS5 username/password authentication (RFC 1929) and refuses SOCKS4. The match is exact and case-sensitive; any other value, including Xray's "noauth", fails with unknown socks auth. |
accounts | array of tables | depends | [] | The accounts accepted when auth = "password", each written as { user = "…", pass = "…" }. Both keys are required in every entry. Needed for auth = "password" to let anyone in: an empty list is accepted by the parser, but then every client is refused. If a user appears twice, the last entry wins. Ignored when auth = "none". |
udp | bool | no | true | Accept the SOCKS5 UDP ASSOCIATE command. When false, the server answers UDP ASSOCIATE with reply code 0x07 (command not supported) and closes the connection. |
udp_bind | string | depends | — | The IP address (IPv4 or IPv6, not a hostname) on which each association binds its UDP relay socket, and which the server reports to the client as the relay address. Without it, the relay uses the local address of the TCP connection the client opened. The relay hears only the address the client's control connection came from, so udp_bind must be in the address family clients connect over: an IPv4 address, or an IPv4-mapped IPv6 one such as ::ffff:192.0.2.10, hears only IPv4 clients, :: hears both, and any other IPv6 address hears only IPv6 clients. A UDP ASSOCIATE from a client the relay could not hear is refused with reply code 0x02 (connection not allowed by ruleset). Required when listen is a Unix socket path and udp is true, because a Unix socket has no local IP. On a Unix socket, each client's UDP ASSOCIATE must also name the exact IP address and port its datagrams will come from, in a family udp_bind hears; a request that leaves either at zero or names a domain is refused with 0x02. |
Unknown keys in [inbound.settings] are rejected, so a typo such as atuh = "password" stops the proxy instead of leaving it open with the default auth = "none".
Authentication and versions
Section titled “Authentication and versions”The inbound detects the version from the first byte and applies auth as follows:
| Client speaks | auth = "none" |
auth = "password" |
|---|---|---|
SOCKS5 offering “no authentication” (0x00) |
accepted | method refused (0xFF) unless it also offers 0x02 |
SOCKS5 offering username/password (0x02) |
method refused (0xFF) unless it also offers 0x00 |
credentials checked against accounts |
| SOCKS4 or SOCKS4a | accepted | refused with reply 91 |
The server selects exactly one method, the one auth names. A client that offers both methods reaches either kind of inbound; curl, for example, offers both when it has credentials. Wrong credentials get the RFC 1929 failure status (0xFF) and the connection closes.
SOCKS4 carries a user ID field; the inbound reads it and ignores it. When the destination IP of a SOCKS4 request starts with 0 (SOCKS4a clients send 0.0.0.x), the inbound reads a hostname after the user ID and passes that hostname on unresolved.
Commands
Section titled “Commands”| Command | Behaviour |
|---|---|
CONNECT (0x01) |
Routed and relayed as a TCP flow. The only command accepted over SOCKS4. |
Tor RESOLVE (0xF0) and RESOLVE_PTR (0xF1) |
SOCKS5 only. Treated as CONNECT to the named address. |
UDP ASSOCIATE (0x03) |
SOCKS5 only. Opens a UDP relay when udp = true; refused with 0x07 otherwise. |
BIND (0x02) |
Refused: 0x07 over SOCKS5, 91 over SOCKS4. |
| Anything else | Refused: 0x07 over SOCKS5, 91 over SOCKS4. |
The whole handshake, from the first byte to the complete request, must arrive within 10 seconds, or the inbound closes the connection.
When the reply is sent
Section titled “When the reply is sent”For a CONNECT, the inbound normally dials the target first and only then answers. The client learns that a target is unreachable from the reply, instead of from a connection that opens and then closes.
The exception is a request that names an IP address while sniffing is on (the default). A client sends nothing before it gets the reply, so the inbound answers “succeeded” at once, collects the client’s first bytes (up to 4 KiB, for at most 300 ms), reads a TLS server name or HTTP Host from them, and uses that domain for routing. Only then does it dial.
sequenceDiagram
participant C as Client
participant S as SOCKS inbound
participant O as Routed outbound
C->>S: Greeting, authentication, CONNECT
alt Target is a domain, or sniffing is off
S->>O: Dial the target
O-->>S: Connected, or an error
S-->>C: Reply 0x00, or a refusal code
else Target is an IP and sniffing is on
S-->>C: Reply 0x00 at once
C->>S: First bytes, up to 4 KiB or 300 ms
S->>O: Dial, routed with the sniffed domain if any
end
C->>O: Relay both ways through the inbound
| Situation | SOCKS5 reply | SOCKS4 reply |
|---|---|---|
| Dial succeeded | 0x00, with the address the client connected to and port 0 |
90 |
| Dial failed with “connection refused” | 0x05 (connection refused) |
91 |
| Any other dial failure | 0x04 (host unreachable) |
91 |
| Dial failed after an early “succeeded” that was followed by client bytes | none: the connection closes | none: the connection closes |
If your clients need an accurate reply for every IP target, set sniffing = false on the inbound; routing then sees only the IP address. Clients that send hostnames, such as curl with socks5h://, are never sniffed and always get the accurate reply.
The inbound checks a relayed connection every 300 seconds and closes it if no data moved in either direction since the last check, so an idle connection lasts between 5 and 10 minutes.
UDP ASSOCIATE
Section titled “UDP ASSOCIATE”With udp = true, a SOCKS5 client can ask for a UDP relay:
- The client sends
UDP ASSOCIATEover its TCP connection, which becomes the control connection. The request’sDST.ADDRandDST.PORTname where the client says its datagrams will come from. The inbound uses them only as described in Which client an association hears. - The inbound picks the relay address:
udp_bindif set, otherwise the local address of the control connection, that is, the address the client reached the server on. Before it binds anything, it checks that a relay on that address can hear the client. If the relay address cannot receive datagrams from the client’s address family (see the table below), it replies0x02(connection not allowed by ruleset) and closes the control connection. Otherwise it binds a new UDP socket on the relay address, on a port the operating system picks, and replies with that address and port. - The client sends datagrams to the relay port, each wrapped in the SOCKS5 UDP header that names its destination.
- The inbound unwraps each datagram and routes it on its own, then wraps every reply with the address of the peer it came from.
Each datagram is routed separately, so one association can reach several peers through different outbounds. A [[route.rule]] with network = "udp", a port or a cidr applies packet by packet, and a datagram routed to blackhole is dropped while the rest of the association keeps working. See Routing. An outbound that carries no UDP drops the datagrams routed to it; Outbounds lists which outbounds carry UDP.
The family check in step 2 follows the relay address:
| Relay address | Hears clients that connected over |
|---|---|
An IPv4 address, or an IPv4-mapped IPv6 address such as ::ffff:192.0.2.10 |
IPv4 only |
:: |
IPv4 and IPv6 |
Any other IPv6 address, including ::1 |
IPv6 only |
So udp_bind = "127.0.0.1" refuses a client that connected over IPv6, and udp_bind = "::1" refuses an IPv4 client. A client that reaches a dual-stack listener over IPv4 counts as IPv4. Without udp_bind, the relay address is the one the client connected to, so it is always in the client’s family.
The inbound drops a datagram that it cannot use:
- a fragmented datagram (a non-zero
FRAGbyte), since fragmentation is not implemented; - a datagram with a malformed header;
- a datagram with an empty payload.
One association keeps a link open to at most 64 different outbounds at a time; when a 65th is needed, the link it sent to least recently is closed.
The association ends when the client closes the control connection, or after 300 seconds with no datagram relayed in either direction. A link to one outbound that ends is dropped on its own and reopened by the next datagram routed to it; the association carries on.
Which client an association hears
Section titled “Which client an association hears”An association relays for one client, the one that opened the control connection (RFC 1928, section 7). Over TCP, the inbound holds it to that client as follows; a Unix-socket client is covered in Listening on a Unix socket:
- The relay accepts datagrams only from the IP address the control connection came from. An IPv4-mapped IPv6 address, such as
::ffff:192.0.2.20, counts as the IPv4 address192.0.2.20. - Datagrams from any other address are dropped and never relayed. Replies go only to the client.
- The first datagram the inbound accepts for relaying pins the association to that datagram’s source port. It must have a valid header,
FRAG0 and a non-empty payload. After that, datagrams from other ports on the same IP are dropped. A malformed or empty datagram does not pin the port. - If the request names the client’s own IP with a non-zero port, that port is held from the start, before any datagram arrives.
- A request that names any other source is set aside, not refused. Clients do this routinely: one behind NAT names its LAN address, sing-box names a loopback address of the other family when its first target is a private address, and PySocks names
0.0.0.0with a port. A domain is set aside too. The association still hears only the control connection’s IP: naming another host never lets that host in.
A client whose UDP leaves from a different IP address than its TCP connection gets no UDP relayed. The association succeeds on TCP and then carries nothing. This happens, for example, when the client reaches the server over IPv6 for TCP and IPv4 for UDP, or sits behind a NAT that maps its TCP and UDP traffic to different public addresses. In the first case, point the client at the server’s IPv4 or IPv6 address instead of a name that resolves to both.
Listening on a Unix socket
Section titled “Listening on a Unix socket”A listen value that starts with / makes the inbound listen on a Unix socket instead of TCP. The socket file is removed when the inbound is released. port must be absent. A Unix socket has no local IP to put the UDP relay on, so with the default udp = true you must choose:
[[inbound]]tag = "local"protocol = "socks"listen = "/run/etemenanki/socks.sock"
[inbound.settings]udp_bind = "127.0.0.1" # or: udp = falseWithout either, --test fails with socks over a unix socket has no local IP for UDP associate; set udp_bind or udp = false. A successful CONNECT over a Unix socket replies with the bound address 0.0.0.0.
A Unix-socket client has no IP address to hold an association to, so its UDP ASSOCIATE must name the exact IP address and non-zero port its datagrams will come from, in a family that udp_bind hears. A request that names an unspecified address (0.0.0.0 or ::), port 0 or a domain is refused with 0x02, and the connection closes. Once the association is granted, the relay accepts datagrams only from exactly that address and port.
Outbound settings
Section titled “Outbound settings”A SOCKS outbound needs server and port for the upstream SOCKS5 server. The common [[outbound]] keys (server, port, stream, address_family) are described on Outbounds. Credentials go in [outbound.settings]:
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
user | string | no | — | Username for SOCKS5 username/password authentication (RFC 1929). When set, the client offers only the password method; when absent, it offers only "no authentication". Longer than 255 bytes is truncated to 255. |
pass | string | no | "" | Password that goes with user. Without user it is ignored and the client does not authenticate. Longer than 255 bytes is truncated to 255. |
[[outbound]]tag = "upstream"protocol = "socks"server = "proxy.example.com"port = 1080
[outbound.settings]user = "alice"pass = "replace-with-a-long-random-password"[[outbound]]tag = "upstream-tls"protocol = "socks"server = "proxy.example.com"port = 443
[outbound.stream]network = "tls"
[outbound.stream.tls]server_name = "proxy.example.com"The outbound supports every transport: TCP, TLS, WebSocket and gRPC, with or without TLS. The peer must terminate that transport before its SOCKS server; etemenanki-app’s own SOCKS inbound accepts plain TCP only.
How the client connects
Section titled “How the client connects”For each TCP flow, the outbound opens a connection to server:port over the configured transport and:
- offers exactly one method: username/password if
useris set, “no authentication” otherwise; - sends the credentials, if any;
- sends a
CONNECTwith the flow’s destination. A domain is sent as a domain, so the upstream server resolves it; - relays the stream once the server replies
0x00.
If the server picks a method other than the one offered, the flow fails with auth method not supported. Rejected credentials fail with server rejects account, and a non-zero reply to the request fails with server rejects request: N, where N is the reply code.
A SOCKS outbound has an upstream address, so it can be a member of a balancer.
UDP through a SOCKS outbound
Section titled “UDP through a SOCKS outbound”A UDP flow routed to a SOCKS outbound opens its own control connection to the server, over the configured transport, and sends UDP ASSOCIATE. The outbound then:
- binds a local UDP socket of the same address family as the relay address the server reported;
- sends every datagram to that relay address, wrapped in the SOCKS5 UDP header;
- accepts replies only from the relay address, and discards datagrams it cannot parse. An IPv4 address and its IPv4-mapped IPv6 form count as the same address in this check;
- ends the association when the control connection closes.
The datagrams themselves travel as plain UDP straight to the relay address, outside the TLS, WebSocket or gRPC transport that carries the control connection. The server must report an IP address it can be reached on: a relay address given as a domain fails with socks: the relay address is a domain.
The UDP ASSOCIATE request names no source (0.0.0.0:0). An upstream that checks sources, as etemenanki-app’s own inbound does, holds the association to the address the control connection came from. UDP through the outbound therefore works only when its datagrams leave from the same IP address as its control connection. If a separate front end terminates the TLS, WebSocket or gRPC transport before the upstream, the upstream sees the control connection come from that front end instead, and drops datagrams that do not come from the front end’s IP address.
Security
Section titled “Security”If you do listen on a public address, as in the example above:
- set
auth = "password"with long random passwords, so the port is not an open relay; - restrict the TCP port and the UDP relay ports to known client addresses with a firewall. Each relay port already hears only the address of its client’s control connection;
- set
udp = falseif no client needs UDP.
Common errors
Section titled “Common errors”These fail --test and startup. --test logs them after configuration invalid:, and each message starts with inbound <tag>: or outbound <tag>:.
| Error | Cause and fix |
|---|---|
unknown socks auth "noauth" |
auth accepts only "none" and "password", in lower case. Xray’s "noauth" is "none" here. |
invalid settings: unknown field `…` |
A misspelled key in [inbound.settings] or [outbound.settings]. The message lists the accepted keys. |
invalid settings: missing field `pass` |
Every entry in accounts needs both user and pass. The next line of the message names accounts. |
invalid settings: invalid IP address syntax |
udp_bind takes an IP address, not a hostname. The next line of the message names udp_bind. |
socks over a unix socket has no local IP for UDP associate; set udp_bind or udp = false |
A Unix socket listen with udp left on. Set one of the two. |
protocol socks does not support stream network "ws" or protocol socks does not support stream security "tls" |
The inbound has an [inbound.stream] block with a network other than tcp, or a security other than none. Remove the block. |
missing server / missing port |
A SOCKS outbound without server or port. With a TLS, WebSocket or gRPC stream and no server, a transport error such as tls stream needs tls.server_name or server can come first. |
These happen at run time. The inbound logs them at debug level when a connection ends, as socks connection from Some(<client IP>) ended: <message>, with None in place of Some(…) over a Unix socket. The outbound reports them as the flow’s error.
| Message | Meaning |
|---|---|
no matching auth method |
The client did not offer the method auth requires; for example, a client without credentials against auth = "password". |
invalid username or password |
The credentials do not match any entry in accounts. |
SOCKS4 not allowed when auth is required |
A SOCKS4 or SOCKS4a client reached an auth = "password" inbound. Switch the client to SOCKS5. |
UDP not enabled |
A client asked for UDP ASSOCIATE while udp = false. |
socks: UDP associate over a unix socket must name its source address and port |
A Unix-socket client’s UDP ASSOCIATE did not name an exact IP address and non-zero port, and got 0x02. Configure the client to name the address and port its UDP comes from. See Listening on a Unix socket. |
socks: UDP associate from an address family the relay is not bound in |
The client connected over an address family that udp_bind cannot receive from, and got 0x02; over a Unix socket, the source the request named is in such a family. Without udp_bind this cannot happen over TCP. Set udp_bind in your clients’ family, or to :: to hear both. See the table in UDP ASSOCIATE. |
TCP bind is not supported |
A client sent BIND. |
client did not complete its request in time |
The client did not complete the handshake within 10 seconds. |
auth method not supported |
Outbound: the upstream server chose a method other than the one offered. Check whether it expects credentials. |
server rejects account |
Outbound: the upstream refused user and pass. |
server rejects request: 5 |
Outbound: the upstream refused the request. The number is the SOCKS5 reply code; 5 means the destination refused the connection. |
server rejects request: 2 |
Outbound, on a UDP flow: the upstream refused the association, for example an etemenanki-app inbound whose udp_bind is not in the family the outbound connected over. |