UdpStatelessSocket
UdpStatelessSocket is a bidirectional UDP edge adapter. It binds one UDP socket, receives datagrams from arbitrary peers, and creates normal Waterwall lines for inbound peer flows.
It exists mainly for packet-oriented nodes such as WireGuardDevice, where the same UDP socket must send to configured peers and also accept packets from peers that contact the socket first. Inbound datagrams are now grouped by peer/local endpoint identity, while non-owned outbound lines still send by routing context.
Typical Placement
WireGuardDevice -> UdpStatelessSocket
UdpStatelessSocket -> WireGuardDevice
TunDevice -> WireGuardDevice -> UdpStatelessSocket
UdpStatelessSocket -> WireGuardDevice -> TunDevice
Use it at the UDP edge of a packet chain when the adjacent node needs one UDP socket for send/receive, and when received peers should have distinct Waterwall line lifetimes. WireGuardDevice is the main example: it writes peer endpoints into destination routing context before sending encrypted datagrams, while received peer datagrams can enter the chain on per-peer lines.
Basic Example
{
"name": "wg-udp-socket",
"type": "UdpStatelessSocket",
"settings": {
"listen-address": "0.0.0.0",
"listen-port": 51820,
"interface": "eth0",
"fwmark": 10,
"large-send-buffer": true,
"large-recv-buffer": true,
"source-ip": "192.0.2.10",
"verbose": false
}
}
Required Settings
| Field | Type | Description |
|---|---|---|
listen-address | string | Local IP address to bind. Must be a valid IP address. |
listen-port | integer | Local UDP port to bind. Valid range is 0 through 65535. |
The top-level settings object must exist and must not be empty.
Optional Settings
| Field | Default | Description |
|---|---|---|
interface | unset | Restricts or pins the socket to a local network device where supported. |
fwmark | unset | Socket mark applied with platform support for SO_MARK. Mainly Linux-oriented. |
source-ip | unset | Overrides the effective local bind address. For this node, it replaces listen-address. |
large-send-buffer | true | true uses Waterwall's default large socket buffer, false leaves the kernel default, and a positive integer requests that byte size. |
large-recv-buffer | true | Same as large-send-buffer, but for SO_RCVBUF. |
verbose | false | Enables per-datagram debug logging for receive and send. |
The default large socket buffer size is currently 4194304 bytes.
Interface And Source-IP Behavior
interface is applied before bind. On Linux, this normally uses device binding. On platforms where binding by device is unavailable, and source-ip is not set, Waterwall falls back to binding the socket to that interface's IPv4 address.
source-ip is treated as an override for listen-address, because this node uses the same socket for both receive and send.
If TunDevice loop protection has published an automatic egress pin, source-ip alone does not override that pin. Use an explicit interface setting when you need to choose a different physical interface.
Receive Path
When a UDP datagram arrives:
- the socket-owning worker receives the datagram
UdpStatelessSocketbuilds a flow key from peer endpoint, local endpoint, and tunnel identity- if no matching flow exists, it creates a normal line and sends
Initinto the connected side of the chain - the sender address and port are written into
routing_context.src_ctx, and the local listener port is recorded - the datagram body is forwarded over that peer's line
If this node is the last node in the chain, received datagrams start from the next/tail side and are sent downstream toward the previous node. Otherwise, they start from the previous/head side and are sent upstream toward the next node.
Idle peer lines use an init timeout of 30 seconds and a keepalive timeout of 300 seconds.
Send Path
When payload reaches UdpStatelessSocket from either direction:
- if the line is an inbound peer line owned by this tunnel, the datagram is sent back to the stored peer endpoint
- otherwise, the destination is read from
line->routing_context.dest_ctx - if the destination is an unresolved domain, async DNS resolution is started
- otherwise, the destination IP and port are converted to a socket address
- the datagram is sent using the bound UDP socket
If the destination context is not ready or has no port, the datagram is dropped and a warning/error is logged.
For unresolved domain destinations, the node caches async DNS results for 30 minutes by domain, port, and domain strategy. This matters because packet-line routing context is scratch metadata and may be overwritten by the next packet.
Worker Ownership
The UDP socket is owned by one worker loop. If a datagram to send is produced on another worker, UdpStatelessSocket schedules the send onto the socket-owning worker and performs the actual sendto() there.
This keeps socket ownership stable while still allowing packet-line traffic to originate from any worker.
Why Not UdpListener Or UdpConnector?
WireGuard-style UDP needs one socket that can both send to a peer and receive from any peer. UdpListener is listener-oriented and UdpConnector is connector-oriented. UdpStatelessSocket keeps the single-socket adapter model, adds per-peer lines for received datagrams, and preserves routing-context sends for non-owned lines.
Packet-Line Semantics
UdpStatelessSocket is an adapter around UDP datagrams. Received datagrams create normal peer lines, not packet-line payload callbacks.
Packet lines are still supported for outbound routing-context sends, and the tunnel does not destroy packet lines during runtime.
Node Metadata
| Property | Value |
|---|---|
| Runtime model | UDP edge adapter with per-peer inbound lines |
| Socket count | One bound UDP socket per node instance |
| Per-peer state | normal line per inbound peer flow |
| Packet-line use | outbound routing-context compatibility |
| Required left padding | 0 |
| DNS cache freshness | 30 minutes |
Common Mistakes
- Sending payload before
dest_ctxcontains a destination IP/domain and port. - Treating received peer lines as packet lines.
- Using it as a general stream adapter. It forwards datagram payloads.
- Relying on
source-ipto overrideTunDeviceloop-protection pinning. Setinterfaceexplicitly. - Forgetting that
fwmarkand interface binding are platform-dependent.