TlsServer
TlsServer is the server-side TLS wrapper for WaterWall. It is built on OpenSSL. It receives encrypted TLS records from
the previous node, performs a real server handshake, decrypts application data for the next node, and encrypts downstream
cleartext back into TLS records for the previous node.
This node behaves like a generic nginx stream TLS terminator, not like nginx http. It does not know about HTTP,
HTTP/2, Trojan, VLESS, or any other application protocol placed after it.
Typical Placement
HTTP server chain:
TcpListener -> TlsServer -> HttpServer
Proxy-protocol server chain:
TcpListener -> TlsServer -> TrojanServer -> TcpUdpConnector
TcpListener -> TlsServer -> VlessServer -> TcpUdpConnector
Generic TLS termination before routing:
TcpListener -> TlsServer -> Router
The next node receives cleartext application data, not TLS records.
Basic Example
[
{
"name": "tls-server",
"type": "TlsServer",
"settings": {
"cert-file": "/etc/waterwall/fullchain.pem",
"key-file": "/etc/waterwall/privkey.pem",
"min-version": "TLSv1.2",
"max-version": "TLSv1.3",
"select-alpns": ["http/1.1"],
"session-cache": "none",
"session-tickets": true,
"verbose": false
},
"next": "http-server"
},
{
"name": "http-server",
"type": "HttpServer",
"settings": {},
"next": "service"
}
]
Fallback Example
{
"name": "tls-server",
"type": "TlsServer",
"settings": {
"cert-file": "/etc/waterwall/fullchain.pem",
"key-file": "/etc/waterwall/privkey.pem",
"fallback-node-name": "nginx-fallback",
"fallback-intentional-delay-ms": 7,
"fallback-intentional-delay-jitter-ms": 1,
"select-alpns": ["http/1.1"]
},
"next": "protected-protocol"
}
Fallback is only used before TlsServer has committed to TLS. It is meant for malformed or plaintext probes on a public
TLS port, not for authenticated application routing.
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 "TlsServer". |
settings | object | Non-empty TLS server settings. |
next | string | Node that receives decrypted cleartext after TLS succeeds. Required in normal use. |
Required settings fields:
| Field | Type | Description |
|---|---|---|
cert-file | string | Path to the server certificate PEM file. |
key-file | string | Path to the server private key PEM file. |
Both files must be valid for OpenSSL context creation.
Optional Settings
| Field | Default | Description |
|---|---|---|
sni | not set | Exact-match SNI gate. Rejects clients that do not send this SNI. |
select-alpns | ["http/1.1"] | Preferred ALPN selection list. |
alpns | not set | Deprecated alias for ALPN selection. Prefer select-alpns. |
min-version | "TLSv1.2" | Minimum TLS version. |
max-version | "TLSv1.3" | Maximum TLS version. |
ciphers | "HIGH:!aNULL:!MD5" | OpenSSL cipher string for TLS 1.2 and earlier. |
prefer-server-ciphers | false | Enables server cipher preference. |
session-timeout | 300 | TLS session lifetime in seconds. |
handshake-timeout-ms | 60000 | Hard deadline for TLS handshake completion. Set 0 to disable. |
session-cache | "none" | Session cache mode. |
session-cache-size | 20480 | Builtin OpenSSL session cache size. |
session-tickets | true | Enables TLS session tickets. |
fallback-node-name | not set | Fallback branch for clearly non-TLS input before TLS commit. Aliases: fallback-node, fallback. |
fallback-intentional-delay-ms | 7 | Delay for upstream payloads sent to fallback. Set 0 to disable. |
fallback-intentional-delay-jitter-ms | 1 | Random jitter around delayed fallback payload scheduling. Ignored when delay is 0. |
tls13-record-shaping | absent | Experimental sender-side TLS 1.3 padding and delay configuration. |
verbose | false | Enables detailed TLS lifecycle logging. |
sni cannot be combined with fallback. A mismatched but otherwise valid ClientHello could otherwise be handed to fallback
as plaintext, which is highly fingerprintable.
Experimental TLS 1.3 Record Shaping
tls13-record-shaping optionally pads and delays the first locally sent TLS 1.3 application records. It is disabled
when absent. It never changes received ciphertext, handshake records, alerts, empty application records, fallback bytes,
or TLS 1.2 records. Configure the local TlsServer and remote TlsClient separately when both sending directions should
be shaped.
Custom form:
"tls13-record-shaping": {
"scope": {"first-application-records": 8},
"outcomes": [
{
"probability": 50,
"padding-bytes": [100, 200],
"delay": {"probability": 75, "ms": [10, 20]}
},
{"probability": 10, "padding-bytes": [400, 600]}
]
}
padding-bytes and delay.ms accept either an integer or an inclusive [minimum, maximum] integer range. There may be
1 through 16 outcomes; first-application-records is 1 through 1024, padding is 1 through 4096 bytes, and delay is 0
through 1000 milliseconds. Outcome probabilities are cumulative, mutually exclusive percentages whose sum may not exceed
100. One roll selects at most one outcome; the unused percentage is an unchanged record. An outcome's delay probability
is rolled only after that outcome is selected. Unknown keys and non-integer or out-of-range values are startup errors.
Every eligible record consumes one scope position even when its roll selects no outcome.
Only the custom scope plus outcomes form is currently accepted. The profile key is rejected until representative
capture, overhead, connection-success, and classifier measurements justify publishing a versioned preset.
Padding is standard zero-valued TLS 1.3 TLSInnerPlaintext padding and adds the selected number of bytes to ciphertext,
subject to the remaining legal TLS record capacity. Delay starts after encryption and adds up to the selected latency.
Records stay in wire order with release_at = max(now + delay, previous_release_at). Queued ciphertext is bounded to 1
MiB per line, with producer backpressure at 768 KiB and release at 384 KiB. Pause and Resume propagation follows the
current wire state: if draining during Resume re-enters with another wire Pause, the stale Resume is suppressed until a
later wire Resume. After peer close_notify finishes the cleartext side, TlsServer sends no Payload, Pause, or Resume
toward it; application bytes received later or already decrypted by an outer callback are discarded. If the cleartext
side finishes, already accepted server ciphertext and an unpadded close_notify drain before downstream
Finish; a wire-side finish cancels and discards queued output and is propagated only toward cleartext. Timer allocation
failure drains immediately in order.
Record shaping can make early sizes and timings less deterministic, but it cannot remove an inner handshake round trip,
make a burst smaller, or hide every direction and timing feature. Combine it with MuxClient/MuxServer when
multiplexing fits the deployment.
TLS Versions
Supported version strings:
TLSv1
TLSv1.1
TLSv1.2
TLSv1.3
TLS1.0
TLS1.1
TLS1.2
TLS1.3
1.0
1.1
1.2
1.3
min-version must not be greater than max-version.
ALPN Selection
TlsServer uses ALPN as a selection hint, not as an access gate.
The configured array order is the server preference order. For example:
"select-alpns": ["h2", "http/1.1"]
If the client offers both values, h2 is selected. If the client sends ALPN but no configured value overlaps, the TLS
handshake continues without negotiated ALPN. If the client sends no ALPN extension, OpenSSL does not call the ALPN
selection callback and the handshake also continues without ALPN.
select-alpns modes:
| Configuration | Behavior |
|---|---|
| omitted | Uses default ["http/1.1"]. |
[] | Disables ALPN selection completely. |
| non-empty array | Selects the first configured value also offered by the client. |
alpns is deprecated but still parsed. Entries may be strings or objects with a value string. Do not configure both
alpns and select-alpns with non-empty values.
ALPN does not tell later tunnels what protocol was chosen. If you advertise h2, the chain behind TlsServer must
actually behave plausibly for HTTP/2 or the deployment can become fingerprintable.
Session Cache
Supported session-cache values:
| Value | Behavior |
|---|---|
"none" | OpenSSL server cache mode with internal lookup/store disabled. This is the default. |
"off" | Disables OpenSSL session caching. |
"builtin" | Enables OpenSSL builtin server session cache using session-cache-size. |
"builtin:SIZE" | Enables builtin cache and sets the cache size inline, for example "builtin:4096". |
shared is intentionally unsupported in the current implementation.
Runtime Behavior
On upstream Init, TlsServer:
- creates per-line OpenSSL state
- allocates memory BIOs
- switches the SSL object into server mode
- arms the handshake deadline unless
handshake-timeout-msis0 - starts the protected
nextbranch immediately if no fallback is configured - otherwise waits for the first bytes to classify TLS versus fallback
Encrypted upstream bytes from the previous node are fed into OpenSSL. After the TLS handshake completes, decrypted
application bytes are forwarded upstream to next.
Downstream cleartext from next is queued until the TLS handshake completes. After that, it is encrypted with
SSL_write() and sent downstream as TLS records.
Establishment Semantics
WaterWall Est represents the underlying transport establishment, not TLS readiness.
TlsServer forwards downstream Est only once. It can happen before the TLS handshake has completed. TLS readiness is
tracked internally and depends on OpenSSL finishing the handshake.
Fallback Behavior
Fallback is selected only before TlsServer has committed to TLS.
The classifier looks at the initial upstream bytes:
- bytes that are clearly not TLS go to fallback
- bytes that start like TLS handshake records, such as
16 03, stay on the TLS path - once OpenSSL produces a ServerHello, the line is committed to the protected TLS branch
When fallback starts, TlsServer releases TLS resources for that line, initializes the fallback branch, and forwards the
saved bytes unchanged after the configured delay and jitter. From then on, payloads pass through fallback without TLS
encryption or decryption.
TLS-looking but malformed, oversized, incomplete, or slow handshakes are closed on the TLS path. They are not routed to fallback after TLS classification.
Fallback delay is applied only to upstream payloads. Downstream responses from fallback are not intentionally delayed. If
upstream Finish arrives while fallback payloads are delayed, the finish waits until the queued fallback payloads have
been delivered.
For public camouflage, the best fallback is usually another real TLS service, often nginx, with similar certificate, SNI, ALPN, cipher, ticket, and protocol-version behavior. A plain HTTP fallback behind a TLS public port is usually easier to fingerprint.
Handshake Timeout
handshake-timeout-ms is a hard deadline that starts when TlsServer initializes the line. It is not refreshed by
received bytes. It is separate from listener idle settings such as TcpListener's initial-idle-timeout-ms and
active-idle-timeout-ms.
Set handshake-timeout-ms to 0 only when you intentionally want no hard TLS handshake deadline.
Finish Behavior
For clean downstream closes, TlsServer sends a TLS close_notify alert when the TLS handshake completed, flushes it,
destroys local TLS state, and then propagates WaterWall Finish downstream.
If the peer sends a clean TLS shutdown, TlsServer treats it as upstream finish toward the next node.
Fatal OpenSSL failures destroy local state and close the relevant directions. In fallback mode, finish is passed through the fallback branch, with upstream finish waiting behind delayed fallback payloads when needed.
Padding
TlsServer does not prepend WaterWall payload bytes directly, so it advertises:
required_padding_left = 0
TLS record construction happens through OpenSSL memory BIO output buffers.
Node Metadata
| Property | Value |
|---|---|
| Node flag | kNodeFlagChainHead |
| Previous node | Allowed, required in normal use |
| Next node | Allowed, required in normal use |
| Layer group | kNodeLayerAnything |
required_padding_left | 0 |
| Line state | OpenSSL SSL object, memory BIOs, handshake flags, fallback state, pending downstream queue |
Nginx Matching Notes
The defaults are intended to resemble a basic nginx stream TLS server with stock-like behavior. Changing these settings
can still produce valid TLS, but it may no longer look like that baseline:
ciphersmin-versionormax-versionprefer-server-cipherssession-cachesession-tickets- ALPN selection
sni- fallback target and fallback timing
If your goal is wire-level similarity to default nginx, keep defaults unless you have measured a reason to change them.
Common Mistakes
- Do not expect
TlsServerto parse HTTP; putHttpServerafter it when you need HTTP behavior. - Do not combine
sniwith fallback. - Do not use ALPN as an authentication or rejection gate; no-overlap ALPN offers continue without negotiated ALPN.
- Do not advertise
h2unless the protected and fallback behavior can plausibly answer HTTP/2. - Do not use
sharedsession cache; it is not implemented.