Skip to main content

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:

FieldTypeDescription
namestringUser-chosen node name. Must be unique inside the config file.
typestringMust be exactly "TlsServer".
settingsobjectNon-empty TLS server settings.
nextstringNode that receives decrypted cleartext after TLS succeeds. Required in normal use.

Required settings fields:

FieldTypeDescription
cert-filestringPath to the server certificate PEM file.
key-filestringPath to the server private key PEM file.

Both files must be valid for OpenSSL context creation.

Optional Settings

FieldDefaultDescription
sninot setExact-match SNI gate. Rejects clients that do not send this SNI.
select-alpns["http/1.1"]Preferred ALPN selection list.
alpnsnot setDeprecated 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-ciphersfalseEnables server cipher preference.
session-timeout300TLS session lifetime in seconds.
handshake-timeout-ms60000Hard deadline for TLS handshake completion. Set 0 to disable.
session-cache"none"Session cache mode.
session-cache-size20480Builtin OpenSSL session cache size.
session-ticketstrueEnables TLS session tickets.
fallback-node-namenot setFallback branch for clearly non-TLS input before TLS commit. Aliases: fallback-node, fallback.
fallback-intentional-delay-ms7Delay for upstream payloads sent to fallback. Set 0 to disable.
fallback-intentional-delay-jitter-ms1Random jitter around delayed fallback payload scheduling. Ignored when delay is 0.
tls13-record-shapingabsentExperimental sender-side TLS 1.3 padding and delay configuration.
verbosefalseEnables 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:

ConfigurationBehavior
omittedUses default ["http/1.1"].
[]Disables ALPN selection completely.
non-empty arraySelects 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:

ValueBehavior
"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:

  1. creates per-line OpenSSL state
  2. allocates memory BIOs
  3. switches the SSL object into server mode
  4. arms the handshake deadline unless handshake-timeout-ms is 0
  5. starts the protected next branch immediately if no fallback is configured
  6. 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

PropertyValue
Node flagkNodeFlagChainHead
Previous nodeAllowed, required in normal use
Next nodeAllowed, required in normal use
Layer groupkNodeLayerAnything
required_padding_left0
Line stateOpenSSL 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:

  • ciphers
  • min-version or max-version
  • prefer-server-ciphers
  • session-cache
  • session-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 TlsServer to parse HTTP; put HttpServer after it when you need HTTP behavior.
  • Do not combine sni with fallback.
  • Do not use ALPN as an authentication or rejection gate; no-overlap ALPN offers continue without negotiated ALPN.
  • Do not advertise h2 unless the protected and fallback behavior can plausibly answer HTTP/2.
  • Do not use shared session cache; it is not implemented.