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": 5000,
"active-idle-timeout-ms": 300000,
"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 integerfalse, or true when mux is present and omittedSets SO_SNDBUF on accepted sockets.
large-recv-bufferboolean or positive integerfalse, or true when mux is present and omittedSets 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, milliseconds5000Timeout before listener-side payload activity is seen.
active-idle-timeout-mspositive integer, milliseconds300000Timeout 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 4194304 bytes.
falseDo not explicitly set this socket buffer; use the kernel default.
positive integerRequest that many bytes directly.

If the option is omitted and the finalized chain contains MuxClient or MuxServer, TcpListener automatically uses the default large socket buffer size for the omitted buffer option.

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.

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": 5000,
"active-idle-timeout-ms": 300000
},
"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 the queued data grows beyond 1 KB, the next node is paused.
  • When the socket becomes writable again, queued buffers are flushed and the next node is resumed.
  • If the queued data grows beyond 16 MB, the connection is closed.

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 activity5000 ms
Active connection after listener-side activity300000 ms

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

The initial and active values are configured with:

{
"settings": {
"initial-idle-timeout-ms": 5000,
"active-idle-timeout-ms": 300000
}
}

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.