Skip to main content

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, when identifier is vless
  • before TrojanServer, or before a TCP relay to an upstream Trojan server, when identifier is trojan

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:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "SoftIpLimiter".
settingsobjectNon-empty SoftIpLimiter settings.
nextstringRequired. Accepted traffic is forwarded to this node.

Required settings:

FieldTypeDescription
identifierstringMust be exactly vless or trojan.
simultaneous-user-limitintegerNumber of distinct source IP addresses allowed for one identity at the same time. Must be from 1 through 6.
tolerance-msintegerMilliseconds after which an inactive IP row is considered stale. Must be at least 1.

Optional Settings

FieldDefaultDescription
on-identification-failurepassthroughWhat 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.
verbosefalseEnables 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

  • SoftIpLimiter is not an authentication node. The following VlessServer or TrojanServer still authenticates.
  • It can run before a local VlessServer/TrojanServer or before a TcpConnector that 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-failure to close to 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-limit is 1, 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 verbose temporarily when you need to confirm whether a connection was rejected or actively closed because of the IP limit.

Node Metadata

PropertyValue
Node flagkNodeFlagNone
Previous nodeRequired
Next nodeRequired
Layer groupkNodeLayer4
Previous node layerkNodeLayer4
Next node layerkNodeLayer4
required_padding_left0

Common Mistakes

  • Placing SoftIpLimiter(identifier=trojan) before TlsServer. 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 SoftIpLimiter after VlessServer or TrojanServer. 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-limit higher than 6.
  • Using it in packet chains.