Skip to main content

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:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "UdpConnector".
settingsobjectDestination and socket configuration.

settings must use exactly one destination style:

StyleRequired fieldsDescription
Single destinationaddress and portEvery line uses the same destination rule.
Weighted destinationsaddressesEach 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:

ValueBehavior
IPv4 stringSend to that IPv4 address.
IPv6 stringSend to that IPv6 address.
Domain stringResolve 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:

ValueBehavior
numberUse that constant port. Must be 1 through 65535.
numeric stringUse 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:

FieldTypeDescription
addressstringSame supported forms as single-destination address.
portnumber or stringSame supported forms as single-destination port, including random(x,y).
weightpositive integerRelative 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.

ValueBehavior
"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​

SettingTypeDefaultDescription
balance-modestring"connection"Weighted destination selection mode: "connection" or "packet".
large-send-bufferboolean or positive integertrueSets SO_SNDBUF on created UDP sockets.
large-recv-bufferboolean or positive integertrueSets SO_RCVBUF on created UDP sockets.
fwmarkintegernot setApplies a Linux-style socket mark through SO_MARK where supported.
interfacestringnot setRestricts outbound sockets to a local network device where supported.
source-ipstringnot setBinds outbound sockets to a specific local source IP with an ephemeral source port.
domain-strategystring or integercore dns.domain-strategySelects how DNS results are chosen for domain destinations.

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 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:

ValueBehavior
"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:

ValueStrategy
0accept-dns-returned-order
1prefer-ipv4
2prefer-ipv6
3only-ipv4
4only-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:

  1. Chooses the destination address and port for the line.
  2. Sets domain-strategy on the line destination context.
  3. Resolves the domain through its internal DomainResolver if needed.
  4. Prepares worker-local UDP resources for the resolved destination.
  5. Applies send and receive buffers plus optional interface, fwmark, source IP, and egress pin settings to any socket it creates.
  6. Starts receive processing as needed before sending downstream est to 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 stateBehavior
At 1 KiB of retained bytes or capacity chargePause the previous node.
Pending writes completeResume the previous node.
Above 16 MiB retained bytes or capacity chargeClose 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​

  • UdpConnector is an outbound chain end and does not need next.
  • DNS resolution is asynchronous and uses the selected domain-strategy.
  • Destination objects under addresses only contain address, port, and weight; socket options are top-level settings.
  • fwmark and device binding are platform-dependent.
  • fwmark is 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.
  • PacketsToStream currently bridges self-consistent IPv4 packets and does not copy mutable per-packet routing context onto its normal output line. Use PacketsToConnection for 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, UdpStatelessSocket may be a better fit than UdpConnector.

Node Metadata​

Source-backed metadata:

PropertyValue
node flagskNodeFlagChainEnd | kNodeFlagSupportsSplice
can_have_prevtrue
can_have_nextfalse
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayerNone
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.

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.