SoftIpLimiter
SoftIpLimiter is a stream middle tunnel that limits how many source IP addresses may use the same early VLESS or
Trojan identity at the same time. It does not need a local Waterwall user database or a list of VLESS/Trojan users; it
keys the limit directly from the identity bytes already present in the user's TCP connection.
It is intended to sit immediately before the clear protocol parser, or before a raw TCP relay to the server that will run that parser:
- before
VlessServer, or before a TCP relay to an upstream VLESS server, whenidentifierisvless - before
TrojanServer, or before a TCP relay to an upstream Trojan server, whenidentifieristrojan
The node reads only the minimum early bytes needed to identify the connection. If the identity is recognized, it checks
the source IP against an in-memory table and then forwards the original buffered bytes unchanged. If the identity cannot
be recognized, the default behavior is to pass the connection through without IP limiting; strict deployments can change
that to close the connection. It does not authenticate users, parse destinations, add headers, remove headers, or rewrite
payload. VlessServer or TrojanServer must still run after it and perform the real protocol authentication.
Typical Placement
VLESS over plain TCP for local or test deployments:
TcpListener -> SoftIpLimiter(identifier=vless) -> VlessServer -> TcpUdpConnector
VLESS over TLS for public deployments:
TcpListener -> TlsServer -> SoftIpLimiter(identifier=vless) -> VlessServer -> TcpUdpConnector
Trojan over TLS:
TcpListener -> TlsServer -> SoftIpLimiter(identifier=trojan) -> TrojanServer -> TcpUdpConnector
Relay in front of an existing upstream VLESS/Trojan server:
TcpListener -> TlsServer -> SoftIpLimiter(identifier=trojan) -> TcpConnector(upstream Trojan server)
Place SoftIpLimiter after any encryption or transport wrapper that hides the VLESS/Trojan bytes, and before the local
protocol server or raw TCP relay that consumes/forwards those bytes. This node is stream-only; do not use it in packet
chains.
Supported Traffic Shape
SoftIpLimiter must receive a plain TCP stream whose first application bytes are the VLESS or Trojan identity bytes.
TLS-wrapped clients are supported only when Waterwall terminates TLS before this node, usually with TlsServer.
Unsupported client-side shapes include WebSocket, gRPC, XHTTP, HTTP Host/header-based wrappers, UDP placement, and
packet chains. In those cases the VLESS/Trojan identity is not at the beginning of the clear TCP stream, so this node
cannot identify the user.
VLESS Example
Allow each VLESS UUID to be active from at most two source IP addresses at a time:
[
{
"name": "listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443
},
"next": "tls-server"
},
{
"name": "tls-server",
"type": "TlsServer",
"settings": {
"cert-file": "/etc/waterwall/fullchain.pem",
"key-file": "/etc/waterwall/privkey.pem"
},
"next": "soft-ip-limit"
},
{
"name": "soft-ip-limit",
"type": "SoftIpLimiter",
"settings": {
"identifier": "vless",
"simultaneous-user-limit": 2,
"tolerance-ms": 30000,
"on-identification-failure": "passthrough",
"verbose": false
},
"next": "vless-server"
},
{
"name": "vless-server",
"type": "VlessServer",
"settings": {
"uuid": "5783a3e7-e373-51cd-8642-c83782b807c5",
"connect": true,
"udp": true
},
"next": "outbound"
},
{
"name": "outbound",
"type": "TcpUdpConnector",
"settings": {
"address": "dest_context->address",
"port": "dest_context->port"
}
}
]
Trojan Example
Allow each Trojan password identity to be active from one source IP address at a time:
[
{
"name": "listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 443
},
"next": "tls-server"
},
{
"name": "tls-server",
"type": "TlsServer",
"settings": {
"cert-file": "/etc/waterwall/fullchain.pem",
"key-file": "/etc/waterwall/privkey.pem"
},
"next": "trojan-soft-ip-limit"
},
{
"name": "trojan-soft-ip-limit",
"type": "SoftIpLimiter",
"settings": {
"identifier": "trojan",
"simultaneous-user-limit": 1,
"tolerance-ms": 45000,
"on-identification-failure": "close",
"verbose": true
},
"next": "trojan-server"
},
{
"name": "trojan-server",
"type": "TrojanServer",
"settings": {
"password": "secret-password",
"connect": true,
"udp": true
},
"next": "outbound"
}
]
Minimal relay example when another server will parse Trojan after Waterwall forwards the stream:
{
"name": "trojan-soft-ip-limit",
"type": "SoftIpLimiter",
"settings": {
"identifier": "trojan",
"simultaneous-user-limit": 1,
"tolerance-ms": 3000,
"verbose": false
},
"next": "tcp-connector"
}
Required Fields
Top-level fields:
| Field | Type | Description |
|---|---|---|
name | string | User-chosen node name. Must be unique inside the config file. |
type | string | Must be exactly "SoftIpLimiter". |
settings | object | Non-empty SoftIpLimiter settings. |
next | string | Required. Accepted traffic is forwarded to this node. |
Required settings:
| Field | Type | Description |
|---|---|---|
identifier | string | Must be exactly vless or trojan. |
simultaneous-user-limit | integer | Number of distinct source IP addresses allowed for one identity at the same time. Must be from 1 through 6. |
tolerance-ms | integer | Milliseconds after which an inactive IP row is considered stale. Must be at least 1. |
Optional Settings
| Field | Default | Description |
|---|---|---|
on-identification-failure | passthrough | What to do when early bytes are definitely not valid for the configured identifier: passthrough forwards the line without IP limiting; close rejects it. Aliases for passthrough: pass, pass-through. |
verbose | false | Enables warning logs for rejected admissions and active payload closures. |
Identity Extraction
In vless mode, the first upstream bytes must start with:
version: 1 byte, must be 00
uuid: 16 raw UUID bytes
The 16 UUID bytes are reduced to the internal 64-bit key with calcHashBytes(uuid, 16).
In trojan mode, the first upstream bytes must begin with:
password hash: 56 ASCII hex bytes, hex(SHA224(password))
SoftIpLimiter validates and decodes the 56 hex characters, then reduces the 28 SHA224 bytes to the internal 64-bit key
with calcHashBytes(sha224, 28).
The identity bytes are not consumed or rewritten. After admission, the complete buffered stream is forwarded unchanged to the next node.
Identification Failure Behavior
SoftIpLimiter waits while the buffer is too short to decide. Once the bytes are definitely invalid for the configured
mode, on-identification-failure decides the next step.
With the default passthrough behavior, the node becomes transparent for that line: no identity row is inserted,
simultaneous-user-limit and tolerance-ms no longer apply, the buffered bytes are replayed unchanged to the next node,
and future payload/lifecycle callbacks pass through normally.
Use passthrough when the following VlessServer or TrojanServer has its own fallback behavior, or when the same chain
is intentionally shared with non-VLESS/Trojan probes. Use close when malformed early data must not reach the next node.
Limit Behavior
The limit is per identity, not global for the node. If simultaneous-user-limit is 2, one UUID or Trojan password hash
may be used by two distinct source IP addresses at the same time. A third active source IP for the same identity is
rejected, but a different identity has its own independent IP list.
Multiple connections from the same source IP and same identity count as one IP address. The row keeps a reference count, so it remains alive until the last live line for that source IP is released or until the row becomes stale.
Tolerance Behavior
tolerance-ms is checked lazily when the table is touched:
- when a new connection is admitted
- before an upstream payload is forwarded
- before a downstream payload is forwarded
- when a line releases its row
There is no background timer. A quiet line may sit idle longer than tolerance-ms; if another connection or payload
touches the same identity and removes that quiet line's IP row, the quiet line is closed the next time it tries to send
or receive payload.
Use a larger tolerance if clients commonly keep long idle connections open. Use a smaller tolerance if stale IP slots should be freed quickly.
Be careful with very large values. With simultaneous-user-limit: 1, if tolerance-ms is set to one hour and a user
moves from mobile data to Wi-Fi, the first IP may keep occupying the identity's only slot for up to one hour after it
stops sending data. For common mobile or roaming clients, a short tolerance such as 3000 to 5000 ms is often a more
practical starting point.
Source IP Requirement
For recognized identities, SoftIpLimiter requires a valid source IP in the WaterWall line source context. Normal stream
listener nodes such as TcpListener provide this.
If the source context is missing or is not an IPv4 or IPv6 address, the connection is rejected.
If identification fails and on-identification-failure is passthrough, no source-IP row is created and this source-IP
requirement is not used for that line.
Logging
The node is silent by default.
When verbose is true, rejected admissions and active payload closures are logged with the identifier mode, internal
64-bit identifier, source IP, worker id, reason, current IP count, and configured limit. Raw VLESS UUIDs and raw Trojan
SHA224 values are not logged.
Operational Notes
SoftIpLimiteris not an authentication node. The followingVlessServerorTrojanServerstill authenticates.- It can run before a local
VlessServer/TrojanServeror before aTcpConnectorthat relays to an upstream VLESS/Trojan server. - It does not need fail2ban or another external limiter; the identity/IP table is maintained inside Waterwall.
- Malformed or unidentified early data is passed through by default. Set
on-identification-failuretocloseto reject it at this node. - The node does not know usernames. It keys only by the early VLESS UUID or Trojan SHA224 value.
- If
simultaneous-user-limitis1, one identity may still open multiple simultaneous lines from the same source IP. - If clients roam between networks, set the limit high enough to cover the expected overlap.
- The table is in tunnel state, shared by workers of this tunnel instance, and protected by an RW lock.
- Enable
verbosetemporarily when you need to confirm whether a connection was rejected or actively closed because of the IP limit.
Node Metadata
| Property | Value |
|---|---|
| Node flag | kNodeFlagNone |
| Previous node | Required |
| Next node | Required |
| Layer group | kNodeLayer4 |
| Previous node layer | kNodeLayer4 |
| Next node layer | kNodeLayer4 |
required_padding_left | 0 |
Common Mistakes
- Placing
SoftIpLimiter(identifier=trojan)beforeTlsServer. Trojan bytes are still encrypted there, so the limiter cannot identify the connection. - Expecting WebSocket, gRPC, XHTTP, or HTTP
Host/header-style client configs to work. - Placing
SoftIpLimiterafterVlessServerorTrojanServer. The limiter is designed to see the raw protocol identity before the parser consumes it. - Expecting it to authenticate the user.
- Expecting default pass-through lines to be limited. They are forwarded without identity rows; use
"on-identification-failure": "close"for strict enforcement. - Setting
simultaneous-user-limithigher than6. - Using it in packet chains.