Skip to main content

TcpConnector

TcpConnector is an outbound TCP client adapter. It receives a WaterWall line from the previous node, chooses a destination from its own settings or from the line routing context, opens a TCP connection, and moves payloads between the chain and the remote server.

Typical placement:

TcpListener -> ... -> TcpConnector -> remote TCP service

This node is normally used as the end of a stream chain.

What It Does​

  • Chooses a destination address and port for each line.
  • Resolves domain names through an internal asynchronous DomainResolver.
  • Opens an outbound TCP socket.
  • Writes upstream payloads from the previous node to the remote socket.
  • Sends remote socket reads back as downstream payloads to the previous node.
  • Emits downstream est after the outbound TCP connection succeeds.
  • Applies optional socket settings such as TCP_NODELAY, TCP_FASTOPEN, socket buffers, SO_MARK, device binding, and source-IP binding when the platform supports them.

TcpConnector behaves like a chain end. It is started by upstream init; it does not need a next node.

Configuration Example​

{
"name": "outbound-tcp",
"type": "TcpConnector",
"settings": {
"address": "example.com",
"port": 443,
"nodelay": true,
"fastopen": false,
"large-send-buffer": true,
"large-recv-buffer": 4194304,
"fwmark": 10,
"interface": "eth0",
"source-ip": "192.0.2.10",
"domain-strategy": "prefer-ipv4"
}
}

Weighted Multi-Destination Example​

{
"name": "outbound-tcp",
"type": "TcpConnector",
"settings": {
"addresses": [
{
"address": "192.0.2.10",
"port": 443,
"weight": 5,
"nodelay": true,
"interface": "eth0",
"domain-strategy": "prefer-ipv4"
},
{
"address": "example.org",
"port": "dest_context->port",
"weight": 1,
"source-ip": "198.51.100.10",
"domain-strategy": "only-ipv4"
},
{
"address": "2001:db8:1::/64",
"port": 443,
"weight": 1,
"interface": null,
"source-ip": null,
"domain-strategy": "only-ipv6"
}
],
"nodelay": true,
"fastopen": false,
"large-send-buffer": true,
"large-recv-buffer": true,
"interface": "eth0",
"domain-strategy": "prefer-ipv4"
}
}

Each new line chooses one object from addresses with probability proportional to its weight. Options omitted inside an address object inherit the top-level value from settings. For interface and source-ip, null disables an inherited top-level value for that destination.

Required Fields​

Top-level fields:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "TcpConnector".
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 chooses one weighted destination object.

Do not mix addresses with top-level address or port.

Destination Selection​

Single Destination​

Use address and port directly under settings.

{
"settings": {
"address": "93.184.216.34",
"port": 443
}
}

Supported address values:

ValueBehavior
IPv4 stringConnect to that IPv4 address.
IPv6 stringConnect to that IPv6 address.
Domain stringResolve the domain before connecting.
"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
numberConnect to that constant port. Must be 1 through 65535.
"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) is not a supported TcpConnector port form in the current parser.

Weighted Destinations​

Use addresses for weighted per-line destination selection.

{
"settings": {
"addresses": [
{
"address": "192.0.2.10",
"port": 443,
"weight": 3
},
{
"address": "198.51.100.20",
"port": 443,
"weight": 1
}
]
}
}

Each object must contain:

FieldTypeDescription
addressstringSame supported forms as single-destination address.
portnumber or special stringSame supported forms as single-destination port.
weightpositive integerRelative probability for choosing this destination.

Each object may also override:

Option
nodelay
fastopen
large-send-buffer
large-recv-buffer
fwmark
interface
source-ip
domain-strategy

The parser also accepts the legacy misspelled key adresses, but new configurations should use addresses. Do not define both names at the same time.

Optional Settings​

SettingTypeDefaultDescription
nodelaybooleantrueEnables TCP_NODELAY on outbound sockets.
fastopenbooleanfalseRequests TCP_FASTOPEN when the platform exposes that option.
large-send-bufferboolean or positive integerchain-dependent when omitted; see belowSets SO_SNDBUF.
large-recv-bufferboolean or positive integerchain-dependent when omitted; see belowSets SO_RCVBUF.
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.

When addresses is used, these fields can be set at the top level as defaults and overridden by each destination object.

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.

When the finalized chain contains either MuxClient or MuxServer, omitted options explicitly request the default large socket buffer size (16 MiB), independently of core misc.tcp-tune or sysctl success. With no Mux node, omitted options use kernel defaults. Explicit settings retain the meanings above, including false. Connector destinations inherit the effective top-level sizes for options they omit.

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

For weighted addresses, a destination object can override domain-strategy for only the lines that select that object.

Interface, Source IP, and Egress Pinning​

interface restricts the outbound 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, connection setup fails.

source-ip binds the outbound socket to a specific local source IP before connect. The address family must match the destination family.

If TunDevice loop protection has published an automatic egress pin and interface is omitted, TcpConnector 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 Remote Destination​

{
"name": "out",
"type": "TcpConnector",
"settings": {
"address": "example.com",
"port": 443,
"domain-strategy": "prefer-ipv4"
}
}

Connect to the Existing Destination Context​

{
"name": "context-out",
"type": "TcpConnector",
"settings": {
"address": "dest_context->address",
"port": "dest_context->port"
}
}

This is useful after nodes that decode, rewrite, or route by destination metadata.

Bind to a Specific Local Address​

{
"name": "egress-ip-a",
"type": "TcpConnector",
"settings": {
"address": "203.0.113.10",
"port": 443,
"source-ip": "192.0.2.10"
}
}

Weighted Egress Pool​

{
"name": "egress-pool",
"type": "TcpConnector",
"settings": {
"addresses": [
{
"address": "edge-a.example.net",
"port": 443,
"weight": 4,
"interface": "eth0"
},
{
"address": "edge-b.example.net",
"port": 443,
"weight": 1,
"interface": "eth1"
}
],
"domain-strategy": "prefer-ipv4"
}
}

Chain Behavior​

During upstream init, TcpConnector initializes its line state, selects the destination rule, copies address and port values into the line destination context, and sets the line's domain strategy.

The internal flow is:

  1. Destination setup chooses address, port, socket options, and optional CIDR randomization data.
  2. The internal DomainResolver resolves the destination if it is a domain.
  3. Once the destination is an IP address with a port, TcpConnector creates the outbound socket and starts an asynchronous connect.
  4. After connect succeeds, TcpConnector sends downstream est to the previous node.
  5. Upstream payloads are written to the socket, and socket reads become downstream payloads.

If destination setup, DNS resolution, socket setup, or connect fails, the line is finished downstream toward the previous node.

Domain Resolution​

Domain resolution is asynchronous. TcpConnector builds an internal DomainResolver chain and stores the selected domain-strategy on the line destination context before resolution.

Payloads that arrive while DNS is pending are held by the resolver. Payloads that arrive after DNS succeeds but before the TCP connect completes are queued by TcpConnector until the socket is ready.

CIDR Randomization on Constant IPs​

A constant IP address may include a CIDR suffix. When a CIDR range is present, TcpConnector randomizes the host part before connecting.

{
"settings": {
"address": "198.51.100.0/24",
"port": 443
}
}
{
"settings": {
"address": "2001:db8:1::/64",
"port": 443
}
}

CIDR suffixes are only valid on constant IP addresses. They are not valid on domains, "src_context->address", or "dest_context->address".

Implementation rules:

Address familyValid prefix lengthNotes
IPv40 through 32/0 randomizes all 32 address bits; /32 keeps the address fixed.
IPv664 through 128/64 randomizes all 64 host bits while preserving the network prefix; /128 keeps the address fixed.

The suffix must contain only decimal digits. Empty, malformed, and out-of-range prefixes are rejected. More specific prefixes randomize only the remaining host bits.

Flow Control and Buffering​

While DNS, connect, or socket writes are pending, TcpConnector may queue upstream payloads.

Current 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 connection and finish the 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. The TCP budget includes both adapter backlog and the active WIO buffer. Partial writes retain the active allocation charge until completion. Overflow closes this connection through its line owner.

These limits prevent unlimited memory growth when the remote side is slow or the connection is not ready yet.

Connection Establishment Timeout​

Each outbound TCP connection attempt has a 60-second deadline, matching NGINX's default proxy_connect_timeout. It starts after address resolution, when the TCP connection attempt begins, and is canceled once the socket connects. Immediate connection errors still close the line earlier. This is separate from the listener's initial idle deadline and the connector's idle cleanup limit; TcpConnector does not have a separate first-payload idle phase.

Idle Timeout​

Each outbound connection is tracked in a worker-local idle table. The current idle timeout is about 600 seconds. Read and write activity refreshes the timeout.

If the connection expires, the socket is closed and downstream finish is sent to the previous node.

This ten-minute cleanup limit matches TcpListener's default active idle timeout. A target that closes sooner still causes downstream finish without waiting for the idle deadline. The limit is not an NGINX maximum; fallback targets allowing longer inactivity require both adapter idle limits to accommodate it.

Notes and Caveats​

  • TcpConnector is an outbound chain end and does not need next.
  • DNS resolution is asynchronous and uses the selected domain-strategy.
  • fwmark, TCP_FASTOPEN, and device binding are platform-dependent.
  • fwmark is not available on Windows.
  • source-ip must be a valid IP address and must match the destination address family.
  • The legacy key adresses is accepted for compatibility, but addresses is the documented spelling.

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