Skip to main content

UserController

UserController is an advanced support node that enforces authenticated-user limits for traffic-serving chains.

Normal users usually should not place it manually. Server-side nodes such as Socks5Server, TrojanServer, and VlessServer create an internal UserController when they run in authenticated database-backed mode. Configure those higher-level nodes first unless you are building a custom tunnel that calls lineAddUser() and understands WaterWall's user-handle lifecycle.

What It Does

UserController sits after a node that has already authenticated a user and recorded a user_handle_t on the line. On each admitted authenticated line, it:

  • checks whether the user is enabled
  • checks expiry state
  • reserves a live outbound connection slot
  • reserves a source-IP slot when source IP is available
  • accounts upstream and downstream payload bytes
  • closes active lines when a user is disabled, expired, removed, or over traffic quota
  • releases connection/IP slots when the line finishes

It does not parse login protocols and it does not store the authoritative user database. It depends on an existing AuthenticationClient node for user lookup, admission checks, and traffic accounting.

Typical Placement

For normal authenticated server nodes, do not add it manually:

TcpListener -> Socks5Server -> TcpConnector

In authenticated mode, Socks5Server inserts its own internal UserController between itself and the configured outbound node.

Manual placement is for advanced custom chains where a previous node records an authenticated user handle:

CustomAuthNode -> UserController -> TcpConnector

Lines without a valid current user handle are passed through without enforcement.

Some nodes authenticate the user only after the line is already open, and their next side is packet lines (one per worker) rather than per-user lines, so they cannot place a UserController after themselves. A WireGuardDevice doing database-backed peer authentication is the motivating example; it places the UserController on its own side that still carries per-peer lines. Depending on which adapter is the chain head, that per-peer line is initiated from either side of the UserController:

UdpStatelessSocket -> UserController -> WireGuardDevice -> TunDevice   (line starts from prev)
TunDevice -> WireGuardDevice -> UserController -> UdpStatelessSocket (line starts from next)

In both layouts the per-peer line passes through unmanaged at first, then WireGuardDevice records the user with lineAddUser() and promotes the line on demand through the Programmatic Promotion API.

UserController is bidirectional: it works whether the line is initiated from its prev side (an upstream Init) or its next side (a downstream Init), records that origin per line, and uses it to account upload/download correctly (see Direction Awareness).

Basic Example

{
"name": "user-controller",
"type": "UserController",
"settings": {
"auth-client-node-name": "auth-client",
"sweep-interval-ms": 1000,
"verbose": false
},
"next": "outbound"
}

auth-client must be an AuthenticationClient node in the same config file. UserController only references that existing node; it does not create one.

Required Fields

FieldTypeDescription
typestringMust be UserController.
settingsobjectMust be a non-empty object.
settings.auth-client-node-namestringName of an existing AuthenticationClient node. It must not point to UserController itself.

Optional Settings

FieldDefaultDescription
sweep-interval-ms1000Per-worker interval for checking active managed lines even when they are idle. Must be >= 1.
verbosefalseEnables more detailed warning context for rejected or closed managed lines.

Enforced Limits

UserController reads the user limit state from the local table maintained by AuthenticationClient.

Limit areaBehavior
enabledDisabled users cannot open new managed lines; existing managed lines are closed by payload checks or the sweep timer.
Time expiryExpired users cannot open new managed lines; existing managed lines are closed.
traffic.up, traffic.down, traffic.totalPayload bytes are added to cumulative counters. Over-quota users are rejected or closed.
connections-outMaximum simultaneous managed outbound lines for the user.
ipsMaximum simultaneous distinct source IPs for the user. Reusing an already-counted IP is allowed.

connections-in is not enforced by this node. Bandwidth speed limiting is also not handled here; use SpeedLimit for bytes-per-second throttling.

A zero or missing limit means unlimited for that field.

Direction Awareness

UserController records, per line, which side the line was initiated from, and uses it to map traffic to the user's upload and download counters correctly. "Upload" is traffic leaving the user toward the far side; "download" is traffic arriving at the user. The user always sits on the side the line started from.

Line originInitiated byUpstream payload counts asDownstream payload counts as
From prev (upstream Init)a head/prev adapter (for example UdpStatelessSocket at the front)uploaddownload
From next (downstream Init)a tail/next adapter (for example UdpStatelessSocket at the back)downloadupload

The forward row is the common case (Socks5Server, TrojanServer, VlessServer). The reverse row happens when the user-facing adapter is the chain tail, as in TunDevice -> WireGuardDevice -> UserController -> UdpStatelessSocket: there the user's packets arrive on the next side, so downstream payload is the user's upload. Quota enforcement is otherwise identical in both directions.

Runtime Flow

On Init from either side (upstream Init for a line from prev, downstream Init for a line from next):

  1. line state is initialized and the origin direction (from prev or from next) is recorded
  2. the current line user handle is read
  3. anonymous or invalid-handle lines are forwarded without enforcement (and may be promoted later, see below)
  4. authenticated lines are checked through AuthenticationClient
  5. accepted lines reserve connection/IP slots and are registered for sweep checks
  6. rejected lines finish back toward the side that initiated them, without opening the far side

Payload bytes are accounted as the user's upload or download based on the line's origin (see Direction Awareness). If accounting says the user should close, the buffer is recycled and both directions are finished.

On Finish, the node releases the reserved connection/IP accounting state, destroys its own line state, and propagates Finish only to the opposite side according to WaterWall's directional finish rules.

Programmatic Promotion API

Some nodes authenticate a user only after the line is already open (see Typical Placement). For them, UserController exposes one function in its public interface.h:

WW_EXPORT user_admission_result_t usercontrollerTunnelTryManageLine(tunnel_t *t, line_t *l);

It runs the same admission as upstream Init — checking every limit and reserving a connection + IP slot — but on demand. A node that wants to use it:

  1. Keeps a pointer to its UserController instance (created the same way Socks5Server builds its internal UserController, via nodeUserControllerGet() and createHandle).
  2. Authenticates the peer and records the user on the line with lineAddUser().
  3. Calls usercontrollerTunnelTryManageLine(user_controller_instance, line).

Return values:

ResultMeaning
kUserAdmissionOkThe line is now managed (admitted), was already managed (idempotent), or carries no user and was left as an unmanaged passthrough. Proceed.
any other kUserAdmission*The user was rejected (disabled, expired, over quota, connection limit, or IP limit). The line is left unmanaged and untouched.

Contract:

  • On a non-OK result the function never sends a finish and never destroys line state. The caller owns the rejection and must close or drop the line itself; it must not treat a rejected line as admitted. A non-OK call reserves nothing, so retrying later is safe.
  • Call it on the line's owner worker (lineGetWID(l) == getWID()), and only on a normal line that already traversed this UserController instance. Do not call it on packet lines.
  • Once a line is managed this way, traffic accounting, the idle sweep, and Finish-time slot release apply exactly as they do for a line admitted at Init; no extra teardown wiring is needed on the caller side.

Example sketch (inside the authenticating node, after a successful handshake on line):

lineAddUser(line, &resolved_handle, username, password);

user_admission_result_t admission = usercontrollerTunnelTryManageLine(ts->user_controller_tunnel, line);
if (admission != kUserAdmissionOk)
{
// limits exceeded (for example IP limit): reject this peer / close the line here.
return;
}
// admitted: continue bringing the peer up.

Sweep Timer

Each worker gets a periodic sweep timer. The sweep checks registered managed lines so disabled, expired, removed, or over-quota users can be disconnected even when no new payload arrives.

The sweep registry holds line references while lines are managed and releases them when the line closes or the worker stops.

Operational Notes

  • Manual placement requires a previous node that calls lineAddUser() with a valid user handle.
  • Authenticated Socks5Server, TrojanServer, and VlessServer create this node internally; do not manually place another UserController immediately after those nodes in that mode.
  • UserController never writes user configuration such as passwords, enabled state, or limits.
  • Live connection/IP counters are process-local runtime state held through AuthenticationClient helpers.
  • Cumulative traffic is added to normal user traffic stats.
  • This is a stream-style support node, not a packet tunnel.

Node Metadata

PropertyValue
Intended audienceAdvanced users and internal authenticated server composition
PositionMiddle stream support node
Requires AuthenticationClientYes
Layer groupAnything to anything
Per-line stateYes
Creates linesNo
Destroys linesNo
Modifies payloadNo
Required left padding0