Socks5Server
Socks5Server is the server side of SOCKS5 inside a Waterwall chain. It accepts SOCKS5 control traffic from the
previous node, performs method negotiation, authenticates through an existing AuthenticationClient node or explicit
no-auth, and then maps SOCKS5 requests into Waterwall traffic.
It supports:
- SOCKS5 method negotiation
- username/password authentication through
AuthenticationClient CONNECTUDP ASSOCIATE- UDP relay associations tied to an accepted TCP control connection
It rejects BIND.
Cached Account Admission
With AuthenticationClient, admission uses its last valid RAM table and clock basis during ordinary network outages and re-authentication. Cached enabled status, expiry, traffic quota and local connection/IP reservations still apply. No initial cache, stopping, terminal accounting-continuity failure or explicit client credential rejection blocks new admission. Only validated authorization clears that rejection; existing authorized traffic keeps cached enforcement and accounting. Offline administrative changes apply after refresh. Local allowances can overshoot aggregate limits across multiple disconnected clients; they are not global reservations.
Typical placement
TCP-only SOCKS5 server:
TcpListener -> Socks5Server -> TcpConnector
SOCKS5 server with combined CONNECT and UDP support:
TcpUdpListener -> [optional middle tunnels] -> Socks5Server -> TcpUdpConnector
There is one previous callback path, not separate TCP and UDP chains into one Socks5Server. TcpUdpListener supplies
the TCP control ingress and its private dynamic UDP provider on that one path. udp: true fails startup when the
finalized preceding path has no UdpListener/TcpUdpListener provider.
Use TcpUdpListener for the normal built-in deployment. TcpListener alone can carry the
SOCKS5 TCP control connection and CONNECT, but it cannot provide the dynamic UDP endpoint required by
UDP ASSOCIATE. A bare UdpListener exposes the provider capability, but by itself cannot accept
the required TCP control connection. It is useful only inside a custom fan-in topology that supplies TCP on the same
callback path.
Socks5Server discovers the provider automatically from its finalized preceding path. There is no provider-name or
provider-API JSON setting.
Do not manually place UserController after an authenticated Socks5Server. Authenticated mode creates and inserts an
internal UserController before the configured next node.
Complete TCP/UDP topology example
This minimal configuration shows all three required data-path roles: the combined entry listener, the SOCKS5 protocol node, and a connector that follows each requested TCP or UDP destination.
{
"name": "socks5-service",
"nodes": [
{
"name": "socks-entry",
"type": "TcpUdpListener",
"settings": {
"address": "127.0.0.1",
"port": 1080,
"nodelay": true
},
"next": "socks-server"
},
{
"name": "socks-server",
"type": "Socks5Server",
"settings": {
"no-auth": true,
"connect": true,
"udp": true,
"ipv4": "127.0.0.1"
},
"next": "outbound"
},
{
"name": "outbound",
"type": "TcpUdpConnector",
"settings": {
"address": "dest_context->address",
"port": "dest_context->port",
"nodelay": true,
"fastopen": false
}
}
]
}
This no-auth example is intentionally bound to loopback. For remote clients,
configure auth-client-node-name with an AuthenticationClient, then replace
both listener and advertised addresses with addresses appropriate for the
deployment.
The addresses and ports in this topology have different jobs:
| Setting or value | Meaning |
|---|---|
TcpUdpListener.settings.address | Local address on which the TCP listener and dynamically created UDP sockets bind. |
TcpUdpListener.settings.port | Public SOCKS5 TCP control port. The internal static UDP child also listens here, but Socks5Server does not accept that fixed port as a SOCKS UDP association. |
Socks5Server.settings.ipv4 | IPv4 address advertised to the SOCKS5 client in BND.ADDR; it must be reachable by that client. It is not the socket bind setting. |
Reply BND.PORT | Per-association UDP relay port chosen dynamically by the operating system and returned by UDP ASSOCIATE. |
Authenticated node example
{
"name": "socks-server",
"type": "Socks5Server",
"settings": {
"auth-client-node-name": "auth-client",
"connect": true,
"udp": true,
"ipv4": "203.0.113.10",
"sweep-interval-ms": 1000,
"verbose": false
},
"next": "outbound"
}
auth-client must be an existing node of type AuthenticationClient in the same configuration. Socks5Server uses
that node for lookups; it does not create the authentication client.
Replace 203.0.113.10 with the IPv4 address advertised to and reachable by your SOCKS5 clients.
No-auth example
{
"name": "socks-server",
"type": "Socks5Server",
"settings": {
"no-auth": true,
"connect": true,
"udp": false
},
"next": "outbound"
}
Exactly one authentication mode must be selected: either auth-client-node-name or no-auth: true.
Required fields
| Field | Type | Description |
|---|---|---|
settings | object | Required and must not be empty. |
settings.auth-client-node-name | string | Required unless no-auth is explicitly true. Must point to an existing AuthenticationClient node and must not point back to the same Socks5Server. |
settings.no-auth | boolean | Required only when no authentication client is configured. Must be true to enable no-auth mode. |
settings.ipv4 / settings.udp-ipv4 | string | Required when udp is enabled. Reachable IPv4 address returned in BND.ADDR of the UDP ASSOCIATE reply; this is not the listener bind address. |
next | string | Required. Points to the real outbound path. |
At least one of connect or udp must be enabled.
When udp is enabled, the finalized preceding path must expose a dynamic UDP provider. In a normal built-in topology,
that means starting the service with TcpUdpListener rather than TcpListener.
Optional fields
| Field | Default | Description |
|---|---|---|
settings.connect | true | Enables SOCKS5 CONNECT. |
settings.udp | false | Enables SOCKS5 UDP ASSOCIATE and UDP datagram handling. |
settings.verbose | false | Enables extra logging, including authentication rejection reasons. |
settings.sweep-interval-ms | 1000 | Forwarded to the internal UserController in authenticated mode. |
The old inline authentication settings username, password, users, and accounts are no longer accepted. If any of
those keys are present, node creation fails. Use AuthenticationServer plus AuthenticationClient, or use explicit
no-auth: true.
Authentication model
When authenticated mode is enabled, the SOCKS5 client must offer username/password authentication. Socks5Server
receives the SOCKS username and password, builds one lookup string in this exact form:
username:password
It then calls the configured AuthenticationClient and checks that combined string as the user's password.
For AuthenticationServer users intended for SOCKS5, store the SOCKS credential pair in the user object's password
field using the exact username:password form. The user object's name is not used for the SOCKS5 wire username; it can
remain operator-facing metadata.
After successful authentication, the user identity and compatible credential
metadata are attached to the Waterwall line. The internal UserController
applies the configured user policy before the line reaches the configured next
node. In no-auth mode no authenticated user identity is available for
user-specific policy, so deployment-level access controls remain important.
SOCKS usernames and passwords must be non-empty in authenticated mode. Embedded NUL bytes are rejected because the authentication lookup key is passed as a C string.
CONNECT behavior
After method negotiation and authentication, a CONNECT request is handled as a normal Waterwall stream path:
- The requested SOCKS destination is copied into
line->dest_ctx. - The destination protocol is set to TCP.
- Upstream
Initis sent to the next node. - Authenticated upstream payload may proceed after adjacent Init, before transport Est. Init reentry preserves older request bytes.
- Every downstream
Estis forwarded immediately, including repeated notifications, without repeating the success reply. - The SOCKS5 success reply precedes backend response bytes; ready reply/body backlogs drain in FIFO order without waiting for Resume.
If connect is disabled, CONNECT requests are rejected with command not supported.
UDP ASSOCIATE behavior
UDP ASSOCIATE creates an association that is owned by the TCP control line. It does not open a normal upstream stream
from the control line.
The provider is discovered automatically by walking the finalized preceding callback path. Optional middle tunnels can
remain between TcpUdpListener and Socks5Server as long as that path still reaches the provider. No provider setting
is added to the Socks5Server JSON.
The UDP associate reply uses:
- the configured
settings.ipv4orsettings.udp-ipv4as the reply address - the dynamically allocated ephemeral UDP port provided by the preceding
UdpListener(normally throughTcpUdpListener) dynamic endpoint provider as the reply port
When UDP ASSOCIATE is requested, Socks5Server queries its upstream listener (UdpListener or TcpUdpListener) to allocate a dynamic, dedicated bound UDP socket endpoint for this client. Once the dynamic endpoint is bound and activated on the current worker, the assigned ephemeral UDP port is returned to the client in the UDP ASSOCIATE reply. If binding, activation, or reply allocation fails, only that association receives a general-failure reply; existing associations remain intact. The returned endpoint must be reachable through firewall and NAT policy. Static/fixed-port UDP listener traffic is not accepted as a SOCKS association.
At UDP Init and again before its first payload, endpoint metadata must name the
line's actual owner WID and an active association handle. A valid-current-worker
metadata/WID mismatch is an ordinary fail-closed UDP-only rejection: it drops
the datagram, sends no reply, and leaves the TCP control line unchanged. A
callback invoked from a different current worker is a fatal core
invariant and is rejected before line state or worker-local resources are read.
UDP security model
The UDP listener is not an open proxy. A UDP datagram is accepted only when:
- the datagram arrives on the dynamic bound UDP socket assigned to that client association
- the datagram source IP/port matches the pinned or expected peer address
- the association was created by a successfully negotiated TCP control line and is active on the current worker
A concrete peer IP in the UDP-associate request that differs from the TCP control peer is rejected. A wildcard peer IP uses the control peer IP; when the requested source port is zero, the first matching datagram pins the port. Standard SOCKS5 does not cryptographically authenticate individual UDP datagrams. A peer sharing the expected public IP that learns a dedicated relay port can therefore race the first packet when the requested port is zero. Stronger protection needs a separately designed authenticated UDP encapsulation or token protocol.
When the TCP control line closes, its dynamic UDP endpoint is closed in UdpListener and its association is removed immediately from the worker-local registry. Later UDP datagrams arriving on that endpoint or referencing that generation are dropped.
Association state is worker-local and uses an opaque dynamic-endpoint identity. It stores copied metadata rather than a usable pointer to the TCP control line, and a closing control line removes only its current matching association.
UDP payload behavior
When a SOCKS UDP datagram arrives:
- The server looks up the matching active association using the opaque dynamic-endpoint identity.
- It validates the SOCKS5 UDP header.
- If
FRAG != 0, it ignores the datagram conservatively. - It parses the requested remote destination from the header.
- It strips the SOCKS5 UDP header and forwards the payload upstream using the requested destination context.
When a reply comes back from the next node:
- The payload is wrapped in a SOCKS5 UDP response header.
- The response header source address identifies the remote endpoint associated with that reply.
- The wrapped datagram is sent downstream toward the dynamic UDP endpoint.
The client-facing association remains separate from outbound processing and uses the normal layer-4 callback contract. Association-owned outbound state is released when the control line closes.
Finish behavior
The implementation follows Waterwall's close ordering:
- local line state is destroyed before real
Finishpropagation - the control line closes its dynamic UDP endpoint and unregisters its UDP association before it can die
- association-owned outbound state is released before close
- final SOCKS5 failure replies, when needed before establishment, are sent through a close path that guards against re-entrant callbacks
Common mistakes
- Do not configure
username,password,users, oraccounts; those legacy keys are rejected. - Do not configure both
auth-client-node-nameandno-auth: true. - Do not enable UDP without
ipv4orudp-ipv4. - Do not use
TcpListener -> Socks5Serverwithudp: true; that is a TCP-only entry and has no dynamic UDP provider. - Do not send SOCKS UDP datagrams to the configured static listener port; use the dynamic
BND.PORTreturned byUDP ASSOCIATE. - Do not manually place
UserControllerimmediately after an authenticatedSocks5Server; it is inserted internally. - Do not expect SOCKS5
BINDor UDP fragmentation reassembly. - Do not allow only the TCP control port through a firewall/NAT when using UDP; the dynamically returned relay port must also be reachable.
Splice Support
Socks5Server supports splice-backed TCP CONNECT payloads. Complete client
deliveries are materialized with sbufEnsureOrdinary() until the TCP setup phase
is established, before parser or nested-input admission. Later TCP payloads and
necessary opaque ordering queues can retain their original buffers. Success
replies still precede backend response bodies, and existing limits remain intact.
UDP client and remote lines request ordinary reads in both directions. A UDP-only service also requests ordinary reads on its TCP control line during Init. In a mixed CONNECT/UDP service, that control-line preference is set when an allowed UDP ASSOCIATE command is parsed. Preferences last for the exact line's lifetime and do not propagate between associated lines. UDP datagrams are still fully materialized before header decoding or framing because already-delivered or transformed input can remain splice-backed.
The internal UserController remains splice-capable; AuthenticationClient is a
lookup dependency rather than a payload hop. Actual splice activation requires
an eligible finalized chain and misc.splice.
Node Metadata
The padding requirement lets UDP replies prepend SOCKS5 datagram headers while preserving Waterwall buffer-padding assumptions. If a reply buffer lacks enough left capacity, the tunnel allocates a replacement buffer and copies the payload before adding the header.
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 262 bytes |
Transport Est and the SOCKS reply are separate obligations. Init/Est reentry cannot overtake older request bytes. TCP Pause/Resume and transport Est propagate repeatedly without suppressing duplicates. Ready success replies and ordered application backlogs do not wait for Resume. A Pause received before next Init is replayed once that side is initialized; a Resume before replay cancels it. UDP association boundaries retain their existing signal mapping. Necessary request/reply ordering queues retain at most 1 MiB and 1,024 buffers per direction; ready direct payloads remain synchronous. Reply admission is published before callbacks so nested data cannot precede the reply.
After a CONNECT success reply has been handed off and next Init, startup Pause replay, and all request/reply ordering work have finished, the server destroys its local state and permanently bypasses itself on that TCP line. Published line credentials remain available. Later callbacks, including Finish, go directly to adjacent active nodes. UDP control, client, and remote lines remain active for association handling.
Control input advances in one synchronous parser scope. Nested deliveries follow older input; method/authentication state and identity are published before reply callbacks. Each negotiation message contains at most 513 metadata bytes. A complete CONNECT header followed by application bytes is accepted like separate deliveries; only genuinely retained application data consumes the shared ordering budget. Empty TCP input is ignored. After UDP ASSOCIATE, TCP control tails are discarded rather than interpreted as datagrams or new CONNECT requests.
An address with a zero-length domain is invalid as soon as its length byte is received. TCP requests receive the unsupported-address reply and close. Such UDP datagrams are discarded without closing the association or creating a backend. Truncated UDP addresses are also discarded within that datagram.
UDP backends are matched by the complete original address kind, exact address or domain bytes, and port within their client association. Hash collisions and later DNS/routing changes do not merge distinct destinations or change removal identity. Domain spelling and case are preserved.