Skip to main content

TcpListener

TcpListener is a TCP server adapter. It binds a local TCP address and port, accepts inbound client connections, creates one WaterWall line for each accepted socket, and passes that line upstream to the next node in the chain.

Typical placement:

client -> TcpListener -> ... -> TcpConnector -> remote service

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

What It Does​

  • Listens on one TCP port, several explicit TCP ports, or a contiguous TCP port range.
  • Accepts inbound TCP clients.
  • Creates a WaterWall line for each accepted socket.
  • Sends client payload upstream to the next node.
  • Sends downstream payload from the chain back to the client socket.
  • Applies optional accept-time filters such as whitelist, blacklist, and balance groups.

TcpListener is a chain-head node. Connections are created by external TCP clients, not by a previous WaterWall tunnel.

Configuration Example​

{
"name": "inbound-listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": [
443,
80,
2083
],
"nodelay": true,
"large-send-buffer": true,
"large-recv-buffer": true,
"interface": "eth0",
"fwmark": 10,
"balance-group": "public-443",
"balance-interval": 30000,
"initial-idle-timeout-ms": 60000,
"active-idle-timeout-ms": 600000,
"multiport-backend": "socket",
"whitelist": [
"192.168.1.0/24",
"2001:db8::/64"
],
"blacklist": [
"192.168.1.50/32"
]
},
"next": "next-node-name"
}

Required Fields​

Top-level fields:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "TcpListener".
nextstringNode that receives accepted TCP lines.

Required settings fields:

FieldTypeDescription
addressstringLocal bind address, such as "0.0.0.0", "::", or a specific local IP.
port or port-rangenumber, array, or range arrayListening port definition. Exactly one of these fields is required.

settings must be a non-empty object.

Port Configuration​

TcpListener supports three port modes.

Single Port​

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

Explicit Port List​

{
"settings": {
"address": "0.0.0.0",
"port": [
443,
80,
2083
]
}
}

This listens only on the listed ports. It does not mean the range from 80 to 2083.

Contiguous Port Range​

{
"settings": {
"address": "0.0.0.0",
"port-range": [
40000,
40100
]
}
}

port-range must be an array with exactly two positive integer ports: [min, max]. The minimum must be lower than or equal to the maximum.

warning

Do not use old string-style range examples such as "40000-40100". The current parser expects port-range as a two-item array.

Rules:

RuleBehavior
Use both port and port-rangeStartup fails.
port numberListens on one port.
port arrayListens on the explicit ports in the array.
port-range arrayListens on every port from minimum to maximum.
Invalid, zero, negative, or above 65535 portStartup fails.

Optional Settings​

SettingTypeDefaultDescription
nodelaybooleanfalseEnables TCP_NODELAY on accepted sockets.
large-send-bufferboolean or positive integerchain-dependent when omitted; see belowSets SO_SNDBUF on accepted sockets.
large-recv-bufferboolean or positive integerchain-dependent when omitted; see belowSets SO_RCVBUF on accepted sockets.
interfacestringnot setRestricts the listener to a local network interface.
fwmarkintegernot setApplies a Linux-style socket mark where supported.
balance-groupstringnot setGroups same-port listeners for client distribution.
balance-intervalinteger, millisecondssocket-manager defaultSticky interval for repeated clients in a balance group.
initial-idle-timeout-mspositive integer, milliseconds60000Timeout before listener-side payload activity is seen.
active-idle-timeout-mspositive integer, milliseconds600000Timeout after listener-side activity is seen.
multiport-backendstringruntime default for port-rangeBackend for contiguous port ranges: "iptables" or "socket".
whitelistarray of stringsnot setAllowed client IPs or CIDR ranges.
blacklistarray of stringsnot setBlocked client IPs or CIDR ranges.

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.

Interface and fwmark​

interface restricts the listener to a local network device.

On Linux, WaterWall uses SO_BINDTODEVICE. On platforms without device binding, WaterWall falls back to binding by the interface IPv4 address when possible.

On a device-bound socket, dispatch considers listeners restricted to that same interface and listeners without an interface restriction. A different interface is ineligible, even with higher ACL priority or a cached balance choice. Unrestricted listeners still compete under the usual address/ACL priorities and registration order; interface scope does not add priority. An unrestricted socket has unknown ingress scope and cannot select an explicitly interface-bound listener. On other platforms, dispatch matches the effective interface bind address instead. Sticky balancing separates protocol and effective interface scope as well as client IP, local address and logical destination port.

fwmark uses SO_MARK on platforms that provide it. It is platform-dependent and is not available on Windows.

Common Examples​

Local Test Listener​

{
"name": "local-entry",
"type": "TcpListener",
"settings": {
"address": "127.0.0.1",
"port": 8080
},
"next": "outbound"
}

This only accepts clients on the same machine.

Public VPS Listener​

{
"name": "public-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443,
"nodelay": true,
"initial-idle-timeout-ms": 60000,
"active-idle-timeout-ms": 600000
},
"next": "tls-server"
}

This accepts IPv4 clients on all local interfaces. The VPS firewall and provider firewall must also allow the selected port.

Multiport Listener​

{
"name": "multiport-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": [
443,
8443,
2083
],
"nodelay": true
},
"next": "router"
}

Explicit port arrays use socket-per-port listening.

Port Range Listener​

{
"name": "range-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port-range": [
40000,
40100
],
"multiport-backend": "socket"
},
"next": "router"
}

For port-range, the runtime can either create one listening socket per port with the socket backend or use a main socket plus redirection rules with the iptables backend. Set multiport-backend explicitly when you need a specific backend.

Accept Path​

When a TCP client connects, TcpListener:

  1. attaches the accepted socket to a worker loop
  2. creates a new WaterWall line
  3. stores listener line state for that connection
  4. fills routing information from the accepted connection
  5. sends upstream init to the next node
  6. starts reading from the client socket

The line source context is populated from the accepted connection. The peer IP is recorded in src_ctx, the local port that accepted the connection is stored in src_ctx.port and local_listener_port, and the client's real TCP source port is stored separately in peer_source_port.

Data Flow​

client socket -> TcpListener -> next node
client socket <- TcpListener <- next node

Directionally:

DirectionBehavior
Client to chainSocket read becomes upstream payload to next.
Chain to clientDownstream payload is written to the accepted socket.

The next node should expect traffic from accepted clients and should send responses back downstream.

Backpressure and Write Queue​

TcpListener protects the process when a client socket is slow or temporarily not writable.

Behavior:

  • If a write cannot complete immediately, outgoing buffers are queued.
  • Once retained bytes or capacity charge reaches 1 KiB, the next node is paused.
  • When the socket becomes writable again, queued buffers are flushed and the next node is resumed.
  • If retained bytes or capacity charge exceeds 16 MiB, the connection is closed.

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.

This prevents one slow client from causing unbounded memory growth.

Idle Timeout Behavior​

Every accepted connection is tracked in a worker-local idle table.

PhaseDefault timeout
Newly accepted connection before listener-side payload activity60000 ms
Active connection after listener-side activity600000 ms

If a connection expires, the socket and line are closed.

The initial and active values are configured with:

{
"settings": {
"initial-idle-timeout-ms": 60000,
"active-idle-timeout-ms": 600000
}
}

The initial duration follows NGINX's default initial wait. The active duration is a ten-minute cleanup limit, aligned with TcpConnector, so fallback connections can follow the target's shorter close timing. A target close propagates through the chain without waiting for the idle deadline, subject to flushing queued output. Ten minutes is not a maximum imposed by NGINX. A shorter explicit listener timeout can still close a fallback connection before its target; targets allowing longer inactivity require both adapter idle limits to accommodate it.

These are generic TCP idle timers: activity extends the deadline without tracking HTTP request stages. TlsServer independently enforces its handshake deadline, 60s by default, and disarms it when entering fallback. HTTP header/body deadlines, response write timeouts, and NGINX lingering-close behavior are not implemented by these two settings.

For chains such as TcpListener -> TlsServer, listener-side activity is not the same thing as a completed TLS handshake. A client that sends a partial TLS record can move into the active timeout while TlsServer is still waiting for more handshake bytes. When tuning probe-resistant TLS fallback behavior, keep these timeouts consistent with the public service timing you want to resemble.

Balance Groups​

balance-group places this listener into a group with other compatible listeners on the same port.

When multiple listeners share the same balance-group and port, the socket manager selects one listener for a new client and remembers that choice using a hash of the client IP. During balance-interval, later connections from the same client IP stay pinned to the same listener.

Example:

{
"settings": {
"address": "0.0.0.0",
"port": 443,
"balance-group": "public-443",
"balance-interval": 30000
}
}

Whitelist and Blacklist​

whitelist and blacklist accept arrays of IP or CIDR strings. Both IPv4 and IPv6 are supported.

Example:

{
"settings": {
"whitelist": [
"10.0.0.0/8",
"192.168.1.20/32",
"2001:db8::/64"
],
"blacklist": [
"10.0.0.13/32"
]
}
}

Behavior:

ConfigurationResult
No listsNo client IP filtering by this listener.
whitelist presentOnly matching client IPs are accepted by this listener.
blacklist presentMatching client IPs are rejected by this listener.
Both presentA client must match the whitelist, if any, and must not match the blacklist.

If multiple listeners are registered on the same port, another listener may still receive the same connection if its filter matches and this listener's filter does not.

Multiport Backend Notes​

For explicit port arrays, WaterWall treats every array entry as one explicit port and uses socket-per-port listening.

For port-range, WaterWall can use:

BackendNotes
"socket"Creates one listening socket per port. Simple and easy to reason about.
"iptables"Uses redirection rules and can be useful for large ranges, but depends on the host environment and firewall rules.

For small ranges, socket is usually easier to debug. For large ranges, iptables can reduce socket count if your deployment controls the system rules.

Filter Priority and Default Paths​

When several TcpListener nodes cover the same local port, the socket manager does not treat every listener as equal. It evaluates more specific listener filters first. A listener with whitelist or blacklist has higher priority than an otherwise equivalent listener without IP filters.

This lets you build a default-path pattern:

same public port
├─ matching filtered listener -> special path
└─ unfiltered listener -> default path

Example:

{
"nodes": [
{
"name": "trusted-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443,
"whitelist": [
"203.0.113.10/32",
"2001:db8:100::/48"
]
},
"next": "trusted-path"
},
{
"name": "default-entry",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443
},
"next": "default-path"
}
]
}

In this shape, clients matching trusted-entry.whitelist are handled by trusted-entry. Other clients fall through to default-entry, because the unfiltered listener behaves like the default path for that port.

You can use the same idea with blacklist to exclude a range from one path and let another listener receive the connection if its own filters match.

TcpListener filters are IP/CIDR filters only. They do not support GeoIP names, domain categories, SNI, HTTP Host, or protocol sniffing. For country-based routing such as geoip:ir, route by a Router node after accepting the connection.

If balance-group is used, matching balanced listeners are selected through the balance-group logic instead of acting as a simple first-match branch. Use the priority/default-path pattern with ordinary filtered listeners when you want deterministic IP-specific routing.

Notes and Caveats​

  • TcpListener is designed as an inbound entry point.
  • The parser expects port as a number or an explicit array of port numbers.
  • The parser expects port-range as a two-item range array.
  • multiport-backend only matters for contiguous port-range listeners.
  • fwmark and device binding are platform-dependent.
  • A listener bound to 127.0.0.1 only accepts local clients.
  • A listener bound to 0.0.0.0 accepts on all IPv4 interfaces, subject to the operating system and provider firewall.

Node Metadata​

Source-backed metadata:

PropertyValue
node flagskNodeFlagChainHead | kNodeFlagSupportsSplice
can_have_prevfalse
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayerNone
layer_group_next_nodekNodeLayer4
required_padding_left0 bytes