Skip to main content

AuthenticationServer

AuthenticationServer is a logical chain-end node for WaterWall user database management. It does not open sockets and does not perform direct network I/O. Instead, it owns an in-memory users_t database, loads and saves that database as JSON, and answers framed logical requests from AuthenticationClient.

Typical placement:

AuthenticationClient -> AuthenticationServer

This node is a chain end and must not have a next node.

What It Does

  • Loads users from db-path into an in-memory users_t table.
  • Falls back to db-path.backup if the primary JSON database cannot be loaded.
  • Rewrites the primary database after successful backup recovery.
  • Periodically saves the current in-memory database.
  • Writes db-path.backup before writing db-path during normal saves.
  • Can write hourly, daily, or weekly historical backup snapshots.
  • Accepts framed request messages from AuthenticationClient.
  • Supports multiple request frames inside one outer message.
  • Dispatches request frames to built-in authentication and user-management modules.
  • Sends one downstream response message containing all response frames for the processed request message.

AuthenticationServer is the authority for users. Other traffic-serving nodes should communicate with it through AuthenticationClient, not by sending server protocol frames directly.

Configuration Example

{
"name": "auth-db",
"type": "AuthenticationServer",
"settings": {
"db-path": "users.json",
"file-save-rate-ms": 10000,
"normal-backups": "daily",
"normal-backups-path": "backups/",
"normal-backups-count-limit": 10,
"session-idle-timeout-ms": 600000,
"verbose": false,
"auth-clients": [
{
"name": "edge-1",
"secret": "long-random-secret",
"allow-stats-push": true,
"allow-user-pull": true,
"allow-user-write": false,
"session-idle-timeout-ms": 600000
}
]
}
}

Required Fields

Top-level fields:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "AuthenticationServer".
settingsobjectDatabase, save, backup, session, and auth-client settings.

Required settings fields:

FieldTypeDescription
db-pathstringPath to the users JSON database file.
file-save-rate-mspositive integerPeriodic save interval in milliseconds.
auth-clientsnon-empty arrayControl-plane clients allowed to authenticate to this server.

AuthenticationServer rejects configurations that define a next node.

Optional Settings

SettingTypeDefaultDescription
session-idle-timeout-mspositive integer600000Default session inactivity timeout in milliseconds.
verbosebooleanfalseEnables focused debug logs for auth flow, request parsing, permissions, and response flow.
normal-backupsstringdisabledHistorical backup mode: "hourly", "daily", or "weekly".
normal-backups-pathstringnot setDirectory for historical backup snapshots. Required when normal-backups is set.
normal-backups-count-limitpositive integer10Maximum historical backup files to keep for this database and backup mode.

normal-backups and normal-backups-path must be specified together. If normal-backups-count-limit is set without normal backups enabled, startup fails.

normal-backups-path may be relative or absolute. The server creates the directory when it does not exist, and startup fails if the path exists but is not a directory.

Auth Clients

auth-clients defines control-plane credentials for AuthenticationClient instances. These are not normal traffic users from the user database.

Each entry must contain:

FieldTypeDescription
namenon-empty stringAuthentication client name.
secretnon-empty stringShared secret for that authentication client.

Optional per-client fields:

FieldTypeDefaultDescription
allow-stats-pushbooleanfalseAllows traffic usage sync modules.
allow-user-pullbooleanfalseAllows user lookup and full-table pull modules.
allow-user-writebooleanfalseAllows user create/update modules.
session-idle-timeout-mspositive integertop-level timeoutOverrides the default timeout for sessions created by this client.

Permission checks are session permissions, not user permissions:

PermissionAllows
allow-user-pullGetAllUsers, user lookup by password, SHA-224, or SHA-256.
allow-stats-pushPushUserStats and UpdateUserTraficStatsDiff.
allow-user-writeAddNewUser and UpdateUser.

Authenticate is public. Every other module requires a valid session token.

Database Format

The recommended on-disk format is an object with a users array:

{
"users": [
{
"id": 1001,
"name": "alice",
"password": "alice:alice-secret",
"email": "alice@example.com",
"wireguard-allowed-ips": "10.44.0.23/32",
"enabled": true,
"limit": {
"traffic": {
"up": 1073741824,
"down": 1073741824,
"total": 2147483648
},
"bandwidth": {
"up": 1048576,
"down": 1048576
},
"ips": 4,
"devices": 2,
"connections-in": 16,
"connections-out": 16
},
"time": {
"created-at-ms": 1735689600000,
"expire-at-ms": 1767225600000
},
"stats": {
"traffic": {
"up": 0,
"down": 0
},
"connections-in": 0,
"connections-out": 0
}
}
]
}

The loader accepts several input layouts:

LayoutDescription
null, empty array, or empty objectAccepted as an empty user table.
[user, ...]Array of user objects.
{"users": [user, ...]}Recommended layout.
{"alice": user, "bob": user}Object map. The key is used as a username hint only when the user has no non-empty name.
standalone user objectAccepted when it contains an id or user-id, plus password or pass.

The saver always rewrites the database as {"users": [...]}.

User Object

A user object must contain:

FieldTypeDescription
id or user-idnon-zero unsigned integerDurable logical user identity. Keep it unique and stable.
password or passstringPassword lookup key used to build derived lookup indexes.

Optional fields:

FieldDescription
nameHuman-readable username. Non-empty names must be unique.
emailUser email metadata.
notesFree-form operator notes.
gid or group-idGroup identifier.
enabled or enableDefaults to true; set false to disable the user.
record-stat-interval-msStatistics record interval.
wireguard-allowed-ipsOptional single WireGuard inner CIDR owned by this user, such as 10.44.0.23/32 or fd00::23/128. Empty, null, or omitted means unset. The field accepts IPv4 and IPv6, is saved in normalized network form, and must not overlap another user's range.
limitUser limits. Missing or zero values mean unlimited.
timeUser time metadata.
statsRuntime statistics, usually maintained by WaterWall.

For Socks5Server, store the final lookup key in password as username:password, for example "alice:alice-secret".

Passwords are stored as plaintext in the JSON database because exported users include the password field. Protect db-path, db-path.backup, and normal backup directories with filesystem permissions appropriate for secret material.

Password-derived lookup keys are not stored directly in JSON. SHA-224, SHA-256, UUID credentials, and WireGuard-style public keys are rebuilt from password while loading users. Two users must not share the same password/SHA-224, SHA-256, UUID credential, or derived WireGuard public key.

wireguard-allowed-ips is durable user configuration, not a password-derived key. It is stored in JSON, synchronized by GetAllUsers, and validated so configured ranges do not overlap between users.

Limits, Time, and Stats

limit may contain:

FieldDescription
trafficup, down, and total byte limits. u and d are accepted aliases.
bandwidthup and down byte-per-second limits. u and d are accepted aliases.
ips or ipMaximum IP count.
devicesMaximum device count.
connections-in or cons-inMaximum inbound connection count.
connections-out or cons-outMaximum outbound connection count.

time may contain:

Field
created-at-ms or created_at_ms
first-usage-at-ms or first_usage_at_ms
expire-at-ms or expires-at-ms
expire-after-first-usage-ms or expire-after-first-use-ms

The same time fields are also accepted at the top level of the user object. Times are milliseconds since the Unix epoch, except expire-after-first-usage-ms, which is a duration.

stats may contain:

FieldDescription
trafficup and down byte counters. u and d are accepted aliases.
speedup and down speed counters. u and d are accepted aliases.
ips or ipRuntime IP count.
devicesRuntime device count.
connections-in or cons-inRuntime inbound connection count.
connections-out or cons-outRuntime outbound connection count.

Large numeric values may be written and read as decimal strings.

Save and Recovery

Every normal save uses a backup-first write:

  1. Build JSON from the current in-memory users table.
  2. Write that JSON to db-path.backup.
  3. Write the same JSON to db-path.

Startup recovery follows this order:

  1. Load and parse db-path.
  2. If that succeeds, use it as the in-memory database.
  3. If it fails, try db-path.backup.
  4. If backup loading succeeds, use it, remove the backup file, and rewrite db-path.
  5. If both primary and backup fail, node creation fails.

Normal backup snapshots are historical operator backups only. They are not used for startup recovery; recovery only uses db-path.backup.

Avoid editing both the primary and backup files by hand while WaterWall is running. The periodic saver may overwrite manual edits with the current in-memory table.

Sessions and Revisions

When an AuthenticationClient authenticates, the server creates an in-memory session with:

  • a 64-byte session token
  • the client name
  • permissions copied from the matching auth-clients entry
  • the client's session idle timeout
  • last activity timestamp
  • a private baseline copy of the current users table
  • baseline config_revision and stats_revision

The server stores one authoritative users table and two store-level revision counters:

RevisionBumped when
config_revisionUser configuration or metadata changes, such as AddNewUser or UpdateUser.
stats_revisionUsage-owned state changes, such as accepted traffic deltas or first-usage stamping.

Revisions start at 1.

The in-memory users table is authoritative: it is the table loaded from disk, periodically saved, and used as the merge target for client updates. Request messages run under the server state rwlock. Independent point reads share the read side, while authentication, baseline replacement, database saves, stats merges, and user mutations take the write side.

Every authenticated request message refreshes the session activity timestamp. Request dispatch holds either the state read or write lock while using the session pointer. Worker 0 takes the state write lock while sweeping idle sessions every 60000 ms, so expiry cannot destroy a session that a request is using. The last-activity timestamp is atomic, allowing concurrent point-read requests to refresh it while sharing the read lock. Sessions are runtime state and are not persisted across process restarts.

Synchronization Model

The intended AuthenticationClient sync flow is:

  1. Authenticate and store the returned token.
  2. Pull the full users table with GetAllUsers.
  3. Keep a local client-side users table for traffic-serving nodes.
  4. Let nodes such as Socks5Server update local runtime usage fields.
  5. Periodically push traffic-stat hints with PushUserStats.
  6. Pull the full table again when server revisions or needs-pull indicate the local copy is stale.

PushUserStats is a narrow traffic-stat merge path. It reads only the password lookup key and traffic counters from each hint; it does not accept arbitrary user metadata updates. User configuration changes must go through management modules guarded by allow-user-write.

Request and Response Framing

Upstream bytes are buffered as a stream. The outer request envelope is:

BytesField
4Unsigned big-endian body size.
64Session token.
remaining bodyOne or more request frames.

The body size includes the 64-byte token plus the request-frame payload. For an unauthenticated Authenticate request, the token may be all zero bytes. Every other request requires a valid token.

Each request frame is:

BytesField
1Request type.
4Correlation ID.
4Unsigned big-endian request data length.
variableRequest data bytes.

The response envelope is:

BytesField
4Unsigned big-endian body size.
8Unsigned big-endian config_revision.
8Unsigned big-endian stats_revision.
remaining bodyOne or more response frames.

Each response frame is:

BytesField
1Response type.
4Correlation ID copied from the request.
4Unsigned big-endian response data length.
variableResponse data bytes.

If a message has a valid session token, it must not contain Authenticate. Malformed envelopes, incomplete frames, oversized payloads, or trailing bytes smaller than a request header cause the logical connection to close safely.

Request Types

TypeModulePermissionRequest dataSuccess response
1pingvalid sessionliteral pingtype 1, data pong
2GetUserBySHA256Hexallow-user-pull64 hex charstype 2, user JSON
3GetUserBySHA256Base64allow-user-pullpadded base64 SHA-256 digesttype 2, user JSON
4GetUserBySHA256allow-user-pullraw 32-byte SHA-256 digesttype 2, user JSON
5GetUserByPasswordallow-user-pullplaintext password lookup keytype 2, user JSON
6AddNewUserallow-user-writeone user JSON objecttype 0, user-added
7UpdateUserallow-user-writefull user JSON objecttype 0, user-updated
8UpdateUserTraficStatsDiffallow-stats-pushfull user JSON objecttype 0, user-traffic-stats-updated
9GetAllUsersallow-user-pullemptytype 3, users JSON plus server-time-ms
10AuthenticatepublicJSON with name and secrettype 4, 64-byte token
11PushUserStatsallow-stats-pushstats hints JSONtype 0, compact status JSON
12GetUserBySHA224Hexallow-user-pull56 hex charstype 2, user JSON
13GetUserBySHA224Base64allow-user-pullpadded base64 SHA-224 digesttype 2, user JSON
14GetUserBySHA224allow-user-pullraw 28-byte SHA-224 digesttype 2, user JSON

Response types:

TypeMeaning
0ok
1pong
2user
3users database
4session token
255error

Unknown request types return an error response with unknown-request-type.

Module Notes

Authenticate

Authenticate expects:

{"name":"edge-1","secret":"long-random-secret"}

If credentials match settings.auth-clients, the server creates a session and returns a 64-byte token. The token is generated from 32 random bytes and encoded as 64 lowercase hexadecimal bytes. Token generation fails closed if secure random bytes are unavailable.

GetAllUsers

GetAllUsers expects an empty payload. On success, it returns the normalized users JSON with response-only server-time-ms metadata:

{"users":[],"server-time-ms":1767225600000}

server-time-ms is not written to the database. It helps AuthenticationClient project server-owned expiry fields onto the client's local clock.

On success, the session baseline is replaced with the returned table and current server revisions.

AddNewUser

AddNewUser expects one user JSON object. The submitted user must contain a non-zero unique id or user-id.

The module rejects duplicate usernames, duplicate ids, and duplicate password-derived lookup keys. On success it saves the database immediately. If that save fails, it attempts to roll back the in-memory add and returns database-save-failed.

UpdateUser

UpdateUser expects a full user JSON object. The password field is used only to find the existing user by SHA-256 password hash. This module does not update the password or password hashes.

The submitted id or user-id must be present, non-zero, and match the existing user found by that password hash. Mutable metadata, limits, time info, stats, and record-stat interval may be updated in memory. Persistence happens on the normal periodic save timer.

UpdateUserTraficStatsDiff

UpdateUserTraficStatsDiff expects a full user JSON object. It uses password and id only to identify and validate the existing user. It then adds only stats.traffic.up and stats.traffic.down as deltas. No other user fields are updated.

If a non-zero traffic delta is accepted and the authoritative user has no first-usage-at-ms, the server stamps it with the current server time.

PushUserStats

PushUserStats accepts one or more partial stats hints. Accepted layouts are:

  • an array of hint objects
  • an object with a users array
  • an object map whose values are hint objects
  • one standalone hint object

Each hint must contain password or pass, and at least one traffic counter:

  • stats.traffic.up
  • stats.traffic.u
  • stats.traffic.down
  • stats.traffic.d

Other fields are ignored. The server compares each present counter with the session baseline, rejects backwards counters, rejects unknown users, rejects duplicate hints for the same password/SHA-256 key, and applies only accepted traffic deltas to the authoritative table.

On success, response data is compact JSON:

{
"status": "stats-updated",
"applied-deltas": 2,
"needs-pull": false,
"config-revision": 1,
"stats-revision": 3
}

When needs-pull is true, AuthenticationClient should call GetAllUsers to refresh its local users table.

Lifecycle Behavior

On upstream init, AuthenticationServer initializes per-line buffer state and sends downstream est toward the previous tunnel.

On upstream payload, it buffers bytes, extracts complete outer messages, processes each contained request frame, and sends one combined response message downstream with tunnelPrevDownStreamPayload().

On upstream finish, it destroys only its own per-line state. It does not call lineDestroy() because it did not create the line.

If the server terminates a logical connection because of malformed or oversized data, it destroys its own line state first and then sends downstream finish toward the previous tunnel.

Notes and Caveats

  • The outer message payload limit is 16 MiB.
  • The request data limit is 16 MiB.
  • The response payload limit is 16 MiB.
  • The queued response limit is 16 MiB.
  • This node does not require left padding and does not prepend in-place.
  • This node is not a packet tunnel and does not use packet-line semantics.
  • The module name UpdateUserTraficStatsDiff intentionally follows the current source spelling.

User Database Performance

  • SHA-256 is the canonical plaintext-password lookup key. A password lookup derives SHA-256 once and resolves through the SHA-256 hash index; there is no fallback scan over the user table, so both hits and misses are average O(1). The exact plaintext comparison after an index hit is the authoritative, collision-safe check.
  • All keyed lookups (SHA-224, SHA-256, UUID, WireGuard public key, durable id, and non-empty name) are average O(1). WireGuard Allowed-IP address lookup and range-overlap detection are O(log M) through an ordered interval index.
  • Session baseline snapshots for GetAllUsers and Authenticate use the native usersCopy() deep copy instead of a JSON round trip. GetAllUsers serializes the store exactly once for the network response and uses a native copy of the same table for the session baseline.
  • Pure point-read request messages (Ping, GetUserBySHA-224/256, and GetUserByPassword) share the server state read lock and may execute concurrently. Any message containing authentication, GetAllUsers, a user mutation, a stats mutation, or an unknown or mixed non-point-read request takes the state write lock for the full dispatch.