UdpConnector
UdpConnector is an outbound UDP client adapter. It manages worker-local UDP sockets,
chooses a destination address and port, and forwards datagrams between the
previous node and the selected remote UDP peer.
Typical placement:
UdpListener -> ... -> UdpConnector -> remote UDP service
This node is normally used as the end of a UDP stream-style chain.
UdpConnector accepts normal layer-4 lines only. A layer-3 worker packet line
must cross an explicit PacketsToStream bridge before reaching this node. Use
StreamToPackets on the receiving side when the payload must return to a packet
chain. For direct packet-line UDP endpoint behavior, use a packet-capable adapter
such as UdpStatelessSocket instead.
What It Does
- Manages outbound UDP sockets in worker-local state.
- Chooses a destination address and port from static settings, weighted destinations, or the line routing context.
- Resolves domain names through an internal asynchronous
DomainResolver. - Sends upstream payloads from the previous node to the remote UDP peer.
- Sends received UDP datagrams back downstream to the previous node.
- Routes received datagrams back to the originating line and rejects unregistered peers.
- Tracks UDP lines with idle timeouts.
- Applies optional socket options such as socket buffers,
SO_MARK, device binding, and source-IP binding when supported.
UdpConnector behaves like a chain end. It is started by upstream init; it
does not need a next node.
Configuration Example
{
"name": "udp-out",
"type": "UdpConnector",
"settings": {
"address": "example.com",
"port": "random(40000,40100)",
"large-send-buffer": true,
"large-recv-buffer": true,
"fwmark": 10,
"interface": "eth0",
"source-ip": "192.0.2.10",
"domain-strategy": "prefer-ipv4"
}
}
Weighted Multi-Destination Example
{
"name": "udp-out",
"type": "UdpConnector",
"settings": {
"balance-mode": "packet",
"addresses": [
{
"address": "1.1.1.1",
"port": 53,
"weight": 3
},
{
"address": "8.8.8.8",
"port": "random(40000,40100)",
"weight": 1
}
],
"large-send-buffer": true,
"large-recv-buffer": true,
"domain-strategy": "prefer-ipv4"
}
}
balance-mode belongs directly inside settings, not inside each object in
addresses.
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 "UdpConnector". |
settings | object | Destination and socket configuration. |
settings must use exactly one destination style:
| Style | Required fields | Description |
|---|---|---|
| Single destination | address and port | Every line uses the same destination rule. |
| Weighted destinations | addresses | Each line or packet chooses one weighted destination object, depending on balance-mode. |
Do not mix addresses with top-level address or port.
Destination Selection
Single Destination
Use address and port directly under settings.
{
"settings": {
"address": "example.com",
"port": 53
}
}
Supported address values:
| Value | Behavior |
|---|---|
| IPv4 string | Send to that IPv4 address. |
| IPv6 string | Send to that IPv6 address. |
| Domain string | Resolve the domain before socket setup. |
"src_context->address" | Copy the source address from the line routing context. |
"dest_context->address" | Copy the destination address from the line routing context. |
Constant domain names must contain between 1 and 255 bytes.
Supported port values:
| Value | Behavior |
|---|---|
| number | Use that constant port. Must be 1 through 65535. |
| numeric string | Use that constant port, for example "53". |
"src_context->port" | Copy the source port from the line routing context. |
"dest_context->port" | Copy the destination port from the line routing context. |
"random(x,y)" | Choose a random port in the inclusive range [x, y]. |
random(x,y) requires both bounds in 1..65535, with x <= y. The complete
bounds are used; for example, random(1000,2000) selects from 1000..2000.
Text after ) is rejected.
Weighted Destinations
Use addresses for weighted destination selection.
{
"settings": {
"addresses": [
{
"address": "1.1.1.1",
"port": 53,
"weight": 3
},
{
"address": "8.8.8.8",
"port": 53,
"weight": 1
}
]
}
}
Each object must contain:
| Field | Type | Description |
|---|---|---|
address | string | Same supported forms as single-destination address. |
port | number or string | Same supported forms as single-destination port, including random(x,y). |
weight | positive integer | Relative probability for choosing this destination. |
Unlike TcpConnector, UdpConnector destination objects do not have per-entry
socket-option overrides in the current implementation. Options such as
large-send-buffer, interface, source-ip, and domain-strategy are
configured at the top level of settings.
The parser also accepts the legacy misspelled key adresses, but new
configurations should use addresses. Do not define both names at the same
time.
Balance Mode
balance-mode controls when weighted destination selection happens.
| Value | Behavior |
|---|---|
"connection" | Default. Choose one weighted destination during upstream init; all payloads on that WaterWall line use that target. |
"packet" | Choose a weighted destination for each upstream payload packet before sending it. |
balance-mode is optional and defaults to "connection".
Normal lines may use IPv4 and IPv6 destinations in "packet" mode. Socket and
local source-port selection are managed internally.
Optional Settings
| Setting | Type | Default | Description |
|---|---|---|---|
balance-mode | string | "connection" | Weighted destination selection mode: "connection" or "packet". |
large-send-buffer | boolean or positive integer | true | Sets SO_SNDBUF on created UDP sockets. |
large-recv-buffer | boolean or positive integer | true | Sets SO_RCVBUF on created UDP sockets. |
fwmark | integer | not set | Applies a Linux-style socket mark through SO_MARK where supported. |
interface | string | not set | Restricts outbound sockets to a local network device where supported. |
source-ip | string | not set | Binds outbound sockets to a specific local source IP with an ephemeral source port. |
domain-strategy | string or integer | core dns.domain-strategy | Selects how DNS results are chosen for domain destinations. |
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 connector sockets.
Domain Strategy
domain-strategy controls how a domain destination is converted to an IP
address when DNS returns IPv4 and IPv6 results.
If omitted, the node uses the core dns.domain-strategy value. If the core
value is also omitted, WaterWall defaults to "prefer-ipv4".
Valid string values:
| Value | Behavior |
|---|---|
"prefer-ipv4" | Choose the first IPv4 result if available; otherwise fall back to IPv6. |
"prefer-ipv6" | Choose the first IPv6 result if available; otherwise fall back to IPv4. |
"only-ipv4" | Use only IPv4 DNS results. If none are returned, resolution is unusable for that line. |
"only-ipv6" | Use only IPv6 DNS results. If none are returned, resolution is unusable for that line. |
"accept-dns-returned-order" | Use the first usable address in the resolver's returned order. |
Legacy integer values are also accepted:
| Value | Strategy |
|---|---|
0 | accept-dns-returned-order |
1 | prefer-ipv4 |
2 | prefer-ipv6 |
3 | only-ipv4 |
4 | only-ipv6 |
Interface, Source IP, and Egress Pinning
interface restricts the UDP socket to a local device.
On Linux, WaterWall uses SO_BINDTODEVICE when available. On platforms without
device binding, it falls back to binding the socket to the interface's IPv4
address. If that fallback cannot find an interface IP, socket setup fails.
source-ip binds the UDP socket to a specific local source IP with source port
0, which asks the OS for an ephemeral port. The address family must match the
selected destination family.
If TunDevice loop protection has published an automatic egress pin and
interface is omitted, UdpConnector applies that pin. In that setup,
source-ip alone does not override the pinned interface; make sure the source IP
belongs to the pinned/default interface, or set interface explicitly.
Common Examples
Fixed DNS Destination
{
"name": "dns-out",
"type": "UdpConnector",
"settings": {
"address": "1.1.1.1",
"port": 53
}
}
Context-Based Destination
{
"name": "context-udp-out",
"type": "UdpConnector",
"settings": {
"address": "dest_context->address",
"port": "dest_context->port"
}
}
This is useful after nodes that fill or rewrite routing context dynamically.
An internal protocol tunnel can mark a negotiated destination as authoritative
for one outbound adapter. In that case UdpConnector preserves the supplied
address and port instead of applying its configured destination selector or
packet balancing. This is internal chain metadata, not a JSON setting; it lets a
SOCKS5 UDP relay use the BND.ADDR/BND.PORT negotiated by its proxy.
Observed-protocol metadata may be cleared by a preceding Router or sniffer,
but this route-control authority survives that narrow reset. UdpConnector
consumes it exactly once during outbound setup and clears it before normal later
routing decisions.
Random Destination Port
{
"name": "random-port-out",
"type": "UdpConnector",
"settings": {
"address": "203.0.113.10",
"port": "random(40000,40100)"
}
}
In "connection" mode, the random port is chosen once during line
initialization and reused for the life of that line.
Packet-Balanced DNS Pool
{
"name": "packet-balanced-dns",
"type": "UdpConnector",
"settings": {
"balance-mode": "packet",
"addresses": [
{
"address": "1.1.1.1",
"port": 53,
"weight": 3
},
{
"address": "8.8.8.8",
"port": 53,
"weight": 1
}
]
}
}
Socket Setup and Lifecycle
During upstream init, UdpConnector:
- Chooses the destination address and port for the line.
- Sets
domain-strategyon the line destination context. - Resolves the domain through its internal
DomainResolverif needed. - Prepares worker-local UDP resources for the resolved destination.
- Applies send and receive buffers plus optional
interface,fwmark, source IP, and egress pin settings to any socket it creates. - Starts receive processing as needed before sending downstream
estto the previous node.
UDP has no connection handshake here. est means the line's local UDP binding
is ready, not that the remote peer has replied.
Domain Resolution
Connection-init DNS resolution is asynchronous. Payloads that arrive before DNS completes are kept in the resolver's bounded pending queue. If resolution fails, the line is finished immediately.
In "packet" balance mode, domain names inside weighted destination objects are
resolved lazily per destination object on each WaterWall line:
- the first packet that selects an unresolved domain starts one async DNS request for that destination object
- packets for that unresolved destination wait in a bounded queue
- after resolution succeeds, destination resources are prepared before the pending queue is flushed; failure drops only that destination queue
- a static destination cache is reused for later packets on the same line; destinations sourced from mutable line context are rebuilt from the current context before use
- if mutable context changes while an earlier DNS request for the same destination slot is pending, the new packet is dropped instead of joining the obsolete request
- there is no time-based DNS cache and no per-packet DNS request
Worker-Local Socket Management
Socket state is worker-local and address-family-aware. Replies are accepted only from expected peer endpoints; unknown sources are dropped. Local socket and source-port behavior is connector-managed and must not be used as line identity. Pause state and idle deadlines remain per line.
Data Flow
Previous node to remote peer:
upstream payload -> UdpConnector -> UDP send
Remote peer to previous node:
UDP receive -> UdpConnector -> downstream payload
In both balance modes, received datagrams are accepted only from registered peers and routed to the originating line.
Flow Control and Buffering
While DNS or packet-destination resolution is pending, UdpConnector may queue
upstream payloads.
Current queue thresholds:
| Queue state | Behavior |
|---|---|
At 1 KiB of retained bytes or capacity charge | Pause the previous node. |
| Pending writes complete | Resume the previous node. |
Above 16 MiB retained bytes or capacity charge | Close the UDP line. |
Queue admission publishes the buffer, accounting and Pause latch before notifying the producer, preserving FIFO under reentrant callbacks. Retention has separate 16 MiB ceilings for logical bytes and buffer-capacity charge (including sbuf, padding and alignment); equality is accepted. Pressure starts when either measure reaches 1 KiB. Empty queued buffers consume capacity charge. Initialization and per-destination DNS queues share one incremental budget per line. Overflow finishes only that association; other associations sharing its socket remain usable. Datagram socket writes retain the existing send/drop policy without a retry queue.
When reads are paused, UdpConnector does not queue received datagrams. A
datagram that arrives while reads are paused is dropped.
Idle Timeout
Idle UDP lines expire automatically, and continuing traffic extends the idle
deadline. On expiry, connector state is released and downstream finish is sent
to the previous node.
Notes and Caveats
UdpConnectoris an outbound chain end and does not neednext.- DNS resolution is asynchronous and uses the selected
domain-strategy. - Destination objects under
addressesonly containaddress,port, andweight; socket options are top-level settings. fwmarkand device binding are platform-dependent.fwmarkis not available on Windows.- Paused reads drop inbound datagrams instead of buffering them.
- Local source ports are connector-managed and must not be used as line identifiers.
PacketsToStreamcurrently bridges self-consistent IPv4 packets and does not copy mutable per-packet routing context onto its normal output line. UsePacketsToConnectionfor per-flow IPv4 TCP/UDP conversion, or a packet-capable UDP adapter when direct packet-line routing semantics are required.- For packet-level, stateless UDP behavior,
UdpStatelessSocketmay be a better fit thanUdpConnector.
Node Metadata
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagChainEnd | kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | false |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayerNone |
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.
This covers connection and packet balancing on normal lines, paused/DNS queues and close-time flushing. Socket retirement removes peer bindings before callbacks and stops a drain if its source owner closes the line. A received Finish is never reflected back during close-time flushing.