Skip to main content

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:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "UdpListener".
nextstringNode that receives UDP peer lines and payloads.

Required settings fields:

FieldTypeDescription
addressstringLocal bind address, such as "0.0.0.0", "::", or a specific local IP.
port or port-rangenumber, array, or range arrayListening 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:

RuleBehavior
Use both port and port-rangeStartup fails.
port numberListens on one UDP port.
port arrayListens on the explicit ports in the array.
port-range arrayListens on every UDP port from minimum to maximum.
Invalid, zero, negative, or above 65535 portStartup fails.
warning

Do not use old string-style ranges such as "20000-20100". The current parser expects port-range as a two-item array.

Optional Settings​

SettingTypeDefaultDescription
large-send-bufferboolean or positive integertrueSets SO_SNDBUF on UDP listener sockets.
large-recv-bufferboolean or positive integertrueSets SO_RCVBUF on UDP listener sockets.
interfacestringnot setRestricts the listener to a local network interface.
fwmarkintegernot setApplies a Linux-style socket mark where supported.
balance-groupstringnot setGroups compatible same-port listeners for sticky distribution.
balance-intervalinteger, millisecondssocket-manager defaultSticky interval for repeated peers in a balance group.
multiport-backendstringruntime default for port-rangeBackend for contiguous port ranges: "iptables" or "socket".
whitelistarray of stringsnot setAllowed peer IPs or CIDR ranges.
blacklistarray of stringsnot setBlocked peer IPs or CIDR ranges.

Socket Buffer Options​

large-send-buffer and large-recv-buffer accept:

ValueMeaning
trueUse WaterWall's default large socket buffer size, currently 16777216 bytes.
falseDo not explicitly set this socket buffer; use the kernel default.
positive integerRequest 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.

SettingBehavior
whitelistIf configured, only matching peer IPs are accepted.
blacklistMatching peer IPs are rejected.
balance-groupEnables sticky peer distribution between compatible listeners on the same port.
balance-intervalControls how long a peer remains sticky to the chosen listener.
multiport-backendControls 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:

  1. A new WaterWall line is created on the first accepted datagram.
  2. The peer address is stored in listener line state.
  3. Later datagrams from the same peer reuse the same line until it expires.
  4. 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:

StateTimeout
Newly created peer lineabout 30 seconds
Active peer after traffic continuesabout 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​

  • UdpListener is a UDP chain-head node and requires next.
  • port arrays are explicit lists, not ranges.
  • Use port-range for contiguous ranges.
  • whitelist and blacklist support IPv4 and IPv6 CIDR strings.
  • fwmark and 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 port or port-range.
  • For built-in SOCKS5 UDP service, configure TcpUdpListener -> ... -> Socks5Server, not a standalone UdpListener or TcpListener entry.
  • For packet-level, stateless UDP behavior, UdpStatelessSocket may be a better fit than UdpListener.

Node Metadata​

Source-backed metadata:

PropertyValue
node flagskNodeFlagChainHead | kNodeFlagSupportsSplice
can_have_prevfalse
can_have_nexttrue
layer_groupkNodeLayerAnything
layer_group_prev_nodekNodeLayerNone
layer_group_next_nodekNodeLayerAnything
required_padding_left0 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.