UdpListener
UdpListener is a UDP server adapter. It binds a local UDP address and port,
receives inbound datagrams, groups them by remote peer, creates one WaterWall
line for each peer, and sends payloads upstream to the next node.
Typical placement:
client -> UdpListener -> ... -> UdpConnector -> remote UDP service
This node is normally used as the beginning of a UDP stream-style chain.
What It Does
- Listens on one UDP port, several explicit UDP ports, or a contiguous UDP port range.
- Receives inbound UDP datagrams through WaterWall's socket manager.
- Applies optional listener filters such as
whitelist,blacklist, and balance groups. - Creates one WaterWall line per remote peer address and port.
- Sends inbound datagrams upstream as payloads to the next node.
- Sends downstream payloads back to the remembered peer address.
- Tracks peer lines with UDP idle timeouts.
UDP itself is connectionless, but UdpListener gives the rest of the chain a
stable line abstraction per peer.
Configuration Example
{
"name": "udp-listener",
"type": "UdpListener",
"settings": {
"address": "0.0.0.0",
"port": [
5353,
853,
123
],
"large-send-buffer": true,
"large-recv-buffer": true,
"interface": "eth0",
"fwmark": 10,
"balance-group": "udp-public",
"balance-interval": 30000,
"multiport-backend": "socket",
"whitelist": [
"192.168.1.0/24",
"2001:db8::/64"
],
"blacklist": [
"192.168.1.50/32"
]
},
"next": "next-node-name"
}
Required Fields
Top-level fields:
| Field | Type | Description |
|---|---|---|
name | string | User-chosen node name. Must be unique inside the config file. |
type | string | Must be exactly "UdpListener". |
next | string | Node that receives UDP peer lines and payloads. |
Required settings fields:
| Field | Type | Description |
|---|---|---|
address | string | Local bind address, such as "0.0.0.0", "::", or a specific local IP. |
port or port-range | number, array, or range array | Listening port definition. Exactly one of these fields is required. |
settings must be a non-empty object.
Port Configuration
UdpListener supports three port modes.
Single Port
{
"settings": {
"address": "0.0.0.0",
"port": 5353
}
}
Explicit Port List
{
"settings": {
"address": "0.0.0.0",
"port": [
5353,
853,
123
]
}
}
This listens only on the listed ports. It does not mean the range from 123 to
5353.
Contiguous Port Range
{
"settings": {
"address": "0.0.0.0",
"port-range": [
20000,
20100
]
}
}
port-range must be an array with exactly two positive integer ports:
[min, max]. The minimum must be lower than or equal to the maximum.
Rules:
| Rule | Behavior |
|---|---|
Use both port and port-range | Startup fails. |
port number | Listens on one UDP port. |
port array | Listens on the explicit ports in the array. |
port-range array | Listens on every UDP port from minimum to maximum. |
Invalid, zero, negative, or above 65535 port | Startup fails. |
Do not use old string-style ranges such as "20000-20100". The current parser
expects port-range as a two-item array.
Optional Settings
| Setting | Type | Default | Description |
|---|---|---|---|
large-send-buffer | boolean or positive integer | true | Sets SO_SNDBUF on UDP listener sockets. |
large-recv-buffer | boolean or positive integer | true | Sets SO_RCVBUF on UDP listener sockets. |
interface | string | not set | Restricts the listener to a local network interface. |
fwmark | integer | not set | Applies a Linux-style socket mark where supported. |
balance-group | string | not set | Groups compatible same-port listeners for sticky distribution. |
balance-interval | integer, milliseconds | socket-manager default | Sticky interval for repeated peers in a balance group. |
multiport-backend | string | runtime default for port-range | Backend for contiguous port ranges: "iptables" or "socket". |
whitelist | array of strings | not set | Allowed peer IPs or CIDR ranges. |
blacklist | array of strings | not set | Blocked peer IPs or CIDR ranges. |
Socket Buffer Options
large-send-buffer and large-recv-buffer accept:
| Value | Meaning |
|---|---|
true | Use WaterWall's default large socket buffer size, currently 16777216 bytes. |
false | Do not explicitly set this socket buffer; use the kernel default. |
| positive integer | Request that many bytes directly. |
Both options default to WaterWall's large socket buffer size for UDP listeners.
The same parsing applies to dynamically opened association sockets: false
leaves their kernel defaults unchanged rather than requesting WaterWall's large
buffer.
Interface and fwmark
interface restricts the listener to a local network device.
On Linux, WaterWall uses SO_BINDTODEVICE. On platforms without device binding,
WaterWall falls back to binding by the interface's IPv4 address when possible.
On a device-bound socket, dispatch considers listeners restricted to that same interface and listeners without an interface restriction. A different interface is ineligible, even with higher ACL priority or a cached balance choice. Unrestricted listeners still compete under the usual address/ACL priorities and registration order; interface scope does not add priority. An unrestricted socket has unknown ingress scope and cannot select an explicitly interface-bound listener. On other platforms, dispatch matches the effective interface bind address instead. Sticky balancing separates protocol and effective interface scope as well as client IP, local address and logical destination port.
fwmark uses SO_MARK on platforms that provide it. It is platform-dependent
and is not available on Windows.
Filtering and Balancing
Filtering and balancing happen in the shared socket manager before the datagram
is delivered to UdpListener.
| Setting | Behavior |
|---|---|
whitelist | If configured, only matching peer IPs are accepted. |
blacklist | Matching peer IPs are rejected. |
balance-group | Enables sticky peer distribution between compatible listeners on the same port. |
balance-interval | Controls how long a peer remains sticky to the chosen listener. |
multiport-backend | Controls how a contiguous port-range listener is implemented. |
When both ACL lists are configured, a peer must match the whitelist, if any, and must not match the blacklist.
Example:
{
"settings": {
"address": "0.0.0.0",
"port": 5353,
"whitelist": [
"10.0.0.0/8",
"203.0.113.10/32",
"2001:db8::/64"
],
"blacklist": [
"10.0.0.99/32"
]
}
}
Per-Peer Line Model
UDP has no transport connection, so UdpListener builds a pseudo-connection
model for the chain.
For each unique remote peer address and port on a listening socket:
- A new WaterWall line is created on the first accepted datagram.
- The peer address is stored in listener line state.
- Later datagrams from the same peer reuse the same line until it expires.
- Downstream payloads on that line are sent back to the remembered peer.
This lets later stream-style nodes work with UDP traffic using normal WaterWall line callbacks.
Data Flow
Peer to chain:
UDP datagram -> UdpListener -> upstream payload -> next node
Chain to peer:
previous downstream payload -> UdpListener -> UDP send to remembered peer
The line source context is filled from the peer socket address and marked as UDP.
Establishment Semantics
A new peer line is created on the first accepted datagram. The line becomes
established only when a downstream est callback reaches UdpListener from the
next node.
Pause Behavior
UdpListener does not queue inbound datagrams while a peer line is paused.
If a datagram arrives from a paused peer line:
- the datagram is dropped
- the line remains alive
- once resumed, new datagrams from that peer can be forwarded again
This is different from TCP-style backpressure. For this node, pause means drop, not buffer.
Idle Timeout
Each peer line is tracked in an idle table.
Current timeouts:
| State | Timeout |
|---|---|
| Newly created peer line | about 30 seconds |
| Active peer after traffic continues | about 300 seconds |
If the peer line expires, UdpListener sends upstream finish and destroys the
line it created.
Dynamic Endpoint Provider
UdpListener also offers a typed, in-process dynamic-endpoint provider for a compatible protocol tunnel in the same
chain. Socks5Server is one consumer of this capability. For a normal SOCKS5 service, use
TcpUdpListener: its internal TcpListener accepts the required TCP control connection and its
internal UdpListener delegates this provider. A bare UdpListener cannot provide a complete SOCKS5 entry by itself
because it cannot accept that TCP control connection.
This is an interface.h capability, not a JSON setting and not an external instance/api.c message API. Socks5Server
discovers it automatically from its finalized preceding path; operators do not configure a provider node name.
The provider creates one dedicated UDP socket with an exclusive port: 0 bind
on the requesting event worker. It returns an opaque value handle (owner WID
plus generation), the kernel-assigned local address, and the actual local port.
The handle is usable only on its owner worker; a stale or repeated close is a
safe no-op and cannot close a newer endpoint.
Dynamic ingress follows the ordinary chain rather than bypassing it:
dynamic UDP socket -> UdpListener -> TcpUdpListener (if present) -> next chain
Downstream replies return through that endpoint to its pinned peer. Dynamic
sockets use the listener address, interface, fwmark, send/receive buffers,
whitelist, and blacklist. They deliberately do not use the static port,
port range, balance group, or multiport backend.
Before a dynamic datagram can create or use its provider-owned line, its normalized peer IP must equal the endpoint's expected peer IP. A configured nonzero source port must match exactly; source port zero pins the first valid sender and every later port must match that pin. Mismatching datagrams are silently dropped and never close or message the TCP control association.
During quiesce, new dynamic opens and activations are refused. On the owner worker the listener first detaches WIO callbacks, then removes each endpoint from its worker-local registry and invokes one idempotent close path for its provider-owned normal line. This endpoint inventory is distinct from the ordinary socket-manager peer table.
Notes and Caveats
UdpListeneris a UDP chain-head node and requiresnext.portarrays are explicit lists, not ranges.- Use
port-rangefor contiguous ranges. whitelistandblacklistsupport IPv4 and IPv6 CIDR strings.fwmarkand device binding are platform-dependent.- Paused peer lines drop inbound datagrams instead of buffering them.
- Dynamic endpoints are internal capability plumbing, not extra public ports
configured through
portorport-range. - For built-in SOCKS5 UDP service, configure
TcpUdpListener -> ... -> Socks5Server, not a standaloneUdpListenerorTcpListenerentry. - For packet-level, stateless UDP behavior,
UdpStatelessSocketmay be a better fit thanUdpListener.
Node Metadata
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagChainHead | kNodeFlagSupportsSplice |
can_have_prev | false |
can_have_next | true |
layer_group | kNodeLayerAnything |
layer_group_prev_node | kNodeLayerNone |
layer_group_next_node | kNodeLayerAnything |
required_padding_left | 0 bytes |
UDP splice sending
UDP receives remain ordinary buffers. On an eligible Linux chain, UDP sends accept ordinary data or a real prefix plus a private-pipe body as one datagram to its explicit destination, including empty datagrams. Supported layouts use pipe-to-socket splice. Highly fragmented pipes, or failure to establish a safe fragment bound before sending, use a complete ordinary fallback before socket assembly. Oversized datagrams are rejected without splitting.
Pressure before assembly retains the existing drop policy, without a UDP retry queue. An incomplete splice or failed commit after priming retires only the affected socket; its associated normal lines close through their owners. Shared-socket peers can therefore close together. No truncated remainder is retried, and unrelated sockets remain active. Ordinary sends retain their existing error policy.
The flag does not bypass platform support, misc.splice, packet exclusion, or support from every node in the final expanded/merged chain. No end-to-end zero-copy or measured performance gain is promised.
Static listener retirement closes admission before notifying peer-owner workers. A canceled notification leaves the existing finite idle deadline and shutdown drain responsible; retired traffic cannot refresh or create peers. Dynamic endpoints detach their handle, WIO and owned line together.