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.

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 integerfalse, or true when mux is present and omittedSets SO_SNDBUF.
large-recv-bufferboolean or positive integerfalse, or true when mux is present and omittedSets 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 4194304 bytes.
falseDo not explicitly set this socket buffer; use the kernel default.
positive integerRequest that many bytes directly.

If either option is omitted and the finalized chain contains MuxClient or MuxServer, TcpConnector automatically uses the default large socket buffer size for that omitted option. Explicit false still disables explicit socket buffer sizing.

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/32 behaves as a fixed address. Prefixes above /32 are rejected.
IPv664 through 128/64 randomizes the lower 64 bits. Prefixes broader than /64 are rejected.

If the computed range is zero, the destination behaves like a fixed address.

Flow Control and Buffering

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

Current thresholds:

Queue stateBehavior
Above 1 KB queuedPause the previous node.
Pending writes completeResume the previous node.
Above 16 MB queuedClose the connection and finish the line.

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

Idle Timeout

Each outbound connection is tracked in a worker-local idle table. The current idle timeout is about 300 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.

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.