Part 1: Core Settings
core.json is the startup configuration file read by the WaterWall executable.
It configures process-wide behavior: logging, worker count, memory profile, MTU,
optional Linux TCP and BBR tuning, external library path, shared DNS resolution, and the
list of node config files to load.
It does not define tunnel nodes directly. Tunnel chains live in the files listed
under configs.
Important Startup Rules
With no startup option or environment override, WaterWall reads ./core.json
from the process working directory. The usual layout is:
waterwall/
WaterWall
core.json
configs/
server.json
Run WaterWall from that directory:
cd ~/waterwall
./WaterWall
To select another core JSON source, use any one of these equivalent CLI forms:
-c:PATH
--c:PATH
-config:PATH
--config:PATH
config:PATH
The colon is required. PATH may be relative to the process working directory
or absolute. The special value stdin reads the core JSON from standard input:
./WaterWall --config:/etc/waterwall/core.json
./WaterWall --config:stdin < core.json
The WW_CORE_JSON_INPUT environment variable accepts the same path or stdin
value. Input selection follows this precedence:
CLI option > WW_CORE_JSON_INPUT > ./core.json
core.json must be valid JSON. Do not put // comments in it. The comment and
variable substitution features described later for node config files are handled
by the node config loader, not by the core settings parser.
Selecting another core input does not change the process working directory. All
paths inside core.json are passed as written, so relative paths—including
entries in configs—remain relative to the directory used to start WaterWall,
not to the selected core file's directory.
Top-Level Structure
{
"log": {},
"misc": {},
"dns": {},
"configs": []
}
| Field | Type | Required | Purpose |
|---|---|---|---|
configs | array of strings | yes | Node config files to parse and run. |
log | object | no | Logger paths, files, log levels, and console output. |
misc | object | no | Worker count, memory profile, MTU, Linux BBR tuning, and library path. |
dns | object | no | Shared async DNS resolver and default domain strategy. |
configs is mandatory and must contain at least one string path. The other
sections are optional, but production deployments should normally keep misc
and dns.domain-strategy explicit.
Do not put domain-strategy at the root of core.json:
{
"domain-strategy": "prefer-ipv4"
}
That form is rejected during startup. Use:
{
"dns": {
"domain-strategy": "prefer-ipv4"
}
}
configs
configs tells WaterWall which node config files to load after the core runtime
is initialized.
{
"configs": [
"configs/server.json",
"configs/reverse.json"
]
}
Rules:
| Rule | Behavior |
|---|---|
Missing configs | Startup fails. |
| Empty array | Startup fails. |
| No string entries | Startup fails. |
| Relative paths | Resolved relative to the process working directory. |
| Multiple files | Loaded by the node manager after core settings are parsed. |
Use one config file for small setups and multiple files when you want to split large deployments into separate chain groups.
log
log configures four process loggers.
| Logger | Purpose |
|---|---|
internal | Low-level runtime and internal diagnostics. |
core | Startup, config parsing, and manager-level logs. |
network | Network and tunnel runtime logs. |
dns | Shared async DNS resolver logs. |
Each logger accepts the same fields:
| Field | Type | Default | Notes |
|---|---|---|---|
loglevel | string | "INFO" | Minimum level written by that logger. |
file | string | logger-specific | File name appended to log.path. |
console | boolean | true | Also print this logger to the console. |
log.path is the base directory used to build log file paths. Its default is
"log/". WaterWall creates the log directory during startup if needed.
Defaults:
| Field | Default |
|---|---|
log.path | "log/" |
log.internal.loglevel | "INFO" |
log.internal.file | "internal.log" |
log.internal.console | true |
log.core.loglevel | "INFO" |
log.core.file | "core.log" |
log.core.console | true |
log.network.loglevel | "INFO" |
log.network.file | "network.log" |
log.network.console | true |
log.dns.loglevel | "INFO" |
log.dns.file | "dns.log" |
log.dns.console | true |
Accepted log levels:
VERBOSE, DEBUG, INFO, WARN, ERROR, FATAL, SILENT
The logger uppercases the configured value before applying it, so "debug" and
"DEBUG" are treated the same by the logger layer.
To show all log levels for one run, add --verbose:
./WaterWall --verbose
./WaterWall --verbose --config:/etc/waterwall/core.json
Equivalent aliases are --log, -log, -verbose, --showlog, -showlog, and
--debug-log. Specify only one spelling per run.
This enables console output and sets internal, core, network, and dns to
VERBOSE, overriding their loglevel and console settings even when logging is
SILENT. Enabled log files also receive the additional messages. File paths and
disabled file outputs still follow the configuration. The override is applied in
memory before logger initialization and does not rewrite core.json. -v still
prints the version. Specify --verbose once; it cannot be combined with a version
or Windows recovery request.
Example:
{
"log": {
"path": "logs/",
"internal": {
"loglevel": "INFO",
"file": "internal.log",
"console": false
},
"core": {
"loglevel": "INFO",
"file": "core.log",
"console": true
},
"network": {
"loglevel": "WARN",
"file": "network.log",
"console": true
},
"dns": {
"loglevel": "DEBUG",
"file": "dns.log",
"console": true
}
}
}
Operational advice:
| Situation | Recommendation |
|---|---|
| First setup or debugging | Keep core, network, and dns console output enabled. |
| Production with many connections | Use INFO or WARN for network; avoid very noisy levels unless debugging. |
| DNS troubleshooting | Temporarily set log.dns.loglevel to "DEBUG". |
| Long-running service | Keep file logging enabled even if console logging is disabled. |
misc
misc controls process-wide runtime sizing and library loading.
{
"misc": {
"workers": 4,
"ram-profile": "server",
"mtu": 1500,
"splice": true,
"tcp-tune": true,
"try-enabling-bbr": true,
"libs-path": "libs/"
}
}
| Field | Type | Default | Validation and behavior |
|---|---|---|---|
workers | integer | CPU core count | 0 or negative falls back to CPU core count. Values above 254 are reduced to 254. |
ram-profile | string or integer | "server" | Selects memory sizing for pools and profile-aware node defaults. A number must be a whole number in 0..6. Invalid values stop startup. |
mtu | integer | 1500 | Construction-time default for per-node MTUs. Must be a whole number in 68..65535. Invalid values stop startup. |
splice | boolean | true | Allow splice on eligible stream chains. false disables splice eligibility for every chain. Platform support and support from every node are still required; packet chains remain ineligible. |
tcp-tune | boolean | true on Linux; false otherwise | Best-effort core startup tuning with 128 MiB socket ceilings and TCP autotuning maxima; also raises RAM-based TCP memory thresholds when splice is enabled. |
try-enabling-bbr | boolean | true | Linux-only best-effort attempt to enable TCP BBR if the running kernel reports BBR support. |
libs-path | string | "libs/" | Directory used when loading external tunnel libraries. |
The defaults above apply whether misc is present, empty, or absent entirely -
including mtu and ram-profile, which an omitted block used to leave unset.
Invalid values are never replaced by a default: they stop startup with the field
named. For predictable deployments, keep the whole misc block explicit anyway.
workers
workers controls how many worker threads WaterWall creates.
On Linux, automatic worker sizing uses the online logical CPU count from
get_nprocs(), excluding configured CPUs that are offline.
Use cases:
| Deployment | Starting point |
|---|---|
| Small client or test instance | 1 or 2 |
| General VPS server | CPU core count |
| Heavy traffic server | CPU core count, then tune from CPU and latency metrics |
More workers are not always better. Each worker has its own event loop and some
per-worker resources. Very high values are capped to 254.
ram-profile
String values:
| Value | Internal profile | Typical use |
|---|---|---|
"server" | L2 memory profile | Default server-side profile with larger pools. |
"client" | M1 memory profile | Generic client-side profile. |
"client-larger" | M2 memory profile | Larger client-side profile. |
"minimal" | S1 memory profile | Minimal memory profile. |
"ultralow" | S1 memory profile | Alias of "minimal". |
The string value is lowercased by the parser before matching. Prefer string values in new configs.
Legacy integer values are also accepted:
| Value | Profile |
|---|---|
0 or 1 | S1 memory profile |
2 | S2 memory profile |
3 | M1 memory profile |
4 | M2 memory profile |
5 | L1 memory profile |
6 | L2 memory profile |
The profile also selects defaults for memory-sensitive node settings. In particular, omitted MuxClient and MuxServer
detached-buffer-limit and detached-child-limit values scale from the S1 minimum to the L2 maximum; explicit node
settings still override them. The Mux node references list the exact limits for every profile.
Invalid numeric or string values stop startup.
mtu
mtu supplies the construction-time default for node MTUs. Each node validates
and stores its own value; overrides never change the core default or other nodes.
It is not one enforced MTU for every node or a measured path MTU.
Use 1500 unless your topology requires a different value. Packet tunnels, TUN
devices, encapsulation layers, and VPN-like chains may need a smaller effective
MTU to avoid fragmentation.
The parser requires a whole number in 68..65535 - RFC 791's minimum IPv4 MTU
up to what the field can hold. A fractional value, a wrong type, or anything
outside that range terminates startup with the field named, rather than being
quietly replaced by a default.
splice
On Linux, keeping both splice and tcp-tune enabled is recommended for
supported stream chains to benefit from splice and the intended startup TCP tuning.
splice defaults to true, including when misc is empty or omitted. Set it to
false to use the ordinary TCP read/write path instead of splice. The setting is
applied when chains are finalized at startup, after internally inserted nodes are
known. Enabling it permits splice only where the platform and every node support
it. Values other than JSON true or false stop startup with a configuration error.
Splice has a process-wide pipe-capacity ceiling selected by ram-profile:
| Profile | Pipe-capacity ceiling |
|---|---|
S1 (minimal, ultralow, numeric 0 or 1) | 32 MiB |
S2 (numeric 2) | 64 MiB |
M1 (client, numeric 3) | 128 MiB |
M2 (client-larger, numeric 4) | 256 MiB |
L1 (numeric 5) | 384 MiB |
L2 (server, numeric 6) | 512 MiB |
When RAM detection is available at startup, the budget is capped further at 20%
of effective maximum RAM: host physical RAM bounded by applicable finite
container/cgroup limits. It uses total capacity rather than currently free RAM,
and rounds down to whole 1 MiB pipes. The RAM cap may reduce the budget below
32 MiB, including to zero. If detection is unavailable, WaterWall warns and uses
the profile ceiling. For example, server with 640 MiB effective RAM gets a
128 MiB budget.
On supported Linux builds, enabled splice with a nonzero budget also makes a
best-effort startup request to raise fs.pipe-user-pages-soft to that budget
converted to system pages, capped by fs.pipe-user-pages-hard when that limit is
nonzero. These settings apply system-wide to each user's pipe usage. WaterWall preserves
an unlimited soft limit (0) and any soft limit already at or above the target;
lack of permission or another failure does not stop startup. It never changes
the hard limit or fs.pipe-max-size, or writes persistent sysctl configuration.
A successful increase remains in effect after exit.
After every startup configuration's chains are finalized, and before prepare/start
hooks or producers run, WaterWall records the fixed maximum required padding and
initializes an empty shared splice inventory. Startup creates no splice buffers
or pipes. The number of pipes that can be created is also capped
at floor((RLIMIT_NOFILE.soft - open_descriptors) / 8): each pipe has two
descriptors, so inventory uses at most one quarter of the remaining descriptor
allowance, leaving the rest for configuration and runtime resources. The
/proc/self/fd count includes its temporary scan descriptor
conservatively; an unavailable limit or scan prevents pipe creation. Startup logs
the configured capacity ceiling and zero allocated capacity. The budget bounds
kernel pipe capacity; it does not reserve that much physical payload memory.
Each worker caches up to eight unused splice buffers. An empty local cache draws
from the shared master, then creates a small refill on demand within the byte
and descriptor ceilings. Each new buffer gets a full-sized 1 MiB private pipe;
smaller pipes are refused. Concurrent workers share the same growth budget.
If no entry can be obtained, callers normally fall back to ordinary buffers,
even if other worker caches retain pipes. Kernel refusals stop growth briefly
without disabling reuse of existing pipes. Healthy pipes are retained until
shutdown. An unusable pipe is retired without restoring its creation allowance.
Chains that never request splice buffers create no pipes. Setting splice to
false skips both pipe-limit tuning and inventory setup.
tcp-tune
tcp-tune defaults to true on native Linux and false on other platforms,
including Android and Cygwin. Omitted or empty misc blocks use the same default.
Only a JSON boolean is accepted. Set "tcp-tune": false to skip this core startup
phase. The four fixed socket/TCP buffer settings apply even when splice is
disabled; the RAM-based tcp_mem step requires both flags to be enabled.
try-enabling-bbr remains independent. On other platforms, explicit true is
accepted but performs no tuning.
After runtime logging is ready and before loading node configurations, WaterWall attempts these four fixed live sysctl writes, independent of the memory profile:
| Setting | Value |
|---|---|
net.core.rmem_max | 134217728 (128 MiB) |
net.core.wmem_max | 134217728 (128 MiB) |
net.ipv4.tcp_rmem | 4096 87380 134217728 |
net.ipv4.tcp_wmem | 4096 87380 134217728 |
When both tcp-tune and splice are enabled, it also tries to increase the
system-wide net.ipv4.tcp_mem thresholds using total host physical RAM and the
system page size. Setting "splice": false skips this step. With
unit = floor(floor(total_ram_bytes / page_size) / 16), the target is
3 * unit, 4 * unit, 6 * unit pages. This preserves the 3:4:6 ratio:
approximately 18.75% of RAM for leaving pressure mode, 25% for entering it,
and 37.5% for the maximum TCP-accounting threshold. These values do not
preallocate memory and do not override cgroup limits.
WaterWall reads the current triple first and preserves it if any target value would lower an existing threshold. A changed triple is read back before success is logged. Invalid or unavailable memory/page-size information, unreadable settings, failed writes, and verification failures are nonfatal and do not skip the other tuning commands. This host-wide target does not use the RAM profile, current free RAM, or a container's memory quota.
This phase does not change net.core.netdev_max_backlog or net.core.somaxconn.
A chain containing either MuxClient or MuxServer activates TcpListener's and
TcpConnector's 16 MiB send/receive defaults for omitted options, independently of
tcp-tune or sysctl success. Without a Mux node, omitted options use kernel defaults.
Explicit large-send-buffer and large-recv-buffer settings always take precedence,
including connector destination overrides.
Each failed command logs a warning, and the remaining commands are still
attempted; permission denial or a missing sysctl never fails startup. Successful
writes change the live kernel settings and are not restored on exit. WaterWall
does not persist them in /etc/sysctl.conf.
RawSocket's separate tuning batch, controlled by skip-sysctl, tunes backlogs and
other TCP parameters. The buffer and tcp_mem writes above belong exclusively to the core
tcp-tune phase.
try-enabling-bbr
When this is true on Linux, WaterWall checks the current TCP congestion
control and net.ipv4.tcp_available_congestion_control. If the running kernel
reports bbr and BBR is not already active, WaterWall applies live sysctl values
for net.core.default_qdisc=fq and net.ipv4.tcp_congestion_control=bbr.
This is a best-effort startup tuning step. WaterWall does not install kernels and
does not edit /etc/sysctl.conf. Set it to false if you do not want WaterWall
to try enabling BBR at startup. tcp-tune controls the separate socket/TCP memory tuning phase.
libs-path
libs-path points to the directory used for external tunnel libraries.
Most normal deployments use the built-in nodes and can leave this as:
{
"misc": {
"libs-path": "libs/"
}
}
Only change it when you intentionally load external node libraries from another directory.
dns
dns configures the shared c-ares based async resolver used by WaterWall
workers. It also sets the default domain address selection strategy used by
connector nodes.
If dns is omitted or empty:
| Setting | Default |
|---|---|
| Resolver options | WaterWall's c-ares defaults |
dns.domain-strategy | "prefer-ipv4" |
timeout-ms | 1000 |
max-timeout-ms | 5000 |
tries | 2 |
query-cache-max-ttl | 1800 |
server-failover.retry-chance | 10 |
server-failover.retry-delay-ms | 5000 |
Other DNS behavior options are passed to c-ares only when configured. If omitted, c-ares and the operating system resolver configuration decide their behavior.
dns.domain-strategy
domain-strategy controls how WaterWall chooses an address when DNS returns IPv4
and/or IPv6 records.
This is the core default for nodes that do not set their own strategy.
TcpConnector and UdpConnector can override it in their own settings.
Weighted TcpConnector.addresses entries can also override it per destination.
Valid string values:
| Value | Behavior |
|---|---|
"prefer-ipv4" | Choose the first IPv4 address if one is returned; otherwise use IPv6. This is the core default. |
"prefer-ipv6" | Choose the first IPv6 address if one is returned; otherwise use IPv4. |
"only-ipv4" | Use only IPv4 results. If no IPv4 address is returned, the result is unusable for that connection. |
"only-ipv6" | Use only IPv6 results. If no IPv6 address is returned, the result is unusable for that connection. |
"accept-dns-returned-order" | Use the first usable address in the order returned by DNS. |
String matching is case-insensitive.
Legacy integer values:
| Value | Strategy |
|---|---|
0 | accept-dns-returned-order |
1 | prefer-ipv4 |
2 | prefer-ipv6 |
3 | only-ipv4 |
4 | only-ipv6 |
Example:
{
"dns": {
"domain-strategy": "prefer-ipv4"
}
}
Practical guidance:
| Environment | Strategy |
|---|---|
| Most IPv4 VPS deployments | "prefer-ipv4" |
| IPv6-first network | "prefer-ipv6" |
| IPv4-only server or firewall rules | "only-ipv4" |
| IPv6-only deployment | "only-ipv6" |
| You trust resolver ordering | "accept-dns-returned-order" |
Resolver Timing and Cache
| Field | Type | Default | Validation | Meaning |
|---|---|---|---|---|
timeout-ms | integer | 1000 | greater than 0 | Initial DNS query timeout in milliseconds. |
max-timeout-ms | integer | 5000 | greater than 0 | Maximum DNS query timeout after retries. |
tries | integer | 2 | greater than 0 | Number of query attempts. |
query-cache-max-ttl | integer | 1800 | 0 or greater | Maximum DNS cache TTL in seconds. |
Example:
{
"dns": {
"timeout-ms": 750,
"max-timeout-ms": 3000,
"tries": 2,
"query-cache-max-ttl": 600
}
}
Lower timeouts make failed domains fail faster but can hurt reliability on slow or lossy networks. A lower cache TTL makes DNS changes visible sooner but increases resolver traffic.
Resolver Behavior Options
These options are optional. When omitted, c-ares defaults apply.
| Field | Type | Validation | Meaning |
|---|---|---|---|
ndots | integer | 0 to 15 | Number of dots required before c-ares tries a name as absolute. |
udp-port | integer | 1 to 65535 | DNS UDP server port. |
tcp-port | integer | 1 to 65535 | DNS TCP server port. |
socket-send-buffer-size | integer | greater than 0 | DNS socket send buffer size. |
socket-receive-buffer-size | integer | greater than 0 | DNS socket receive buffer size. |
edns-packet-size | integer | 1 to 65535 | EDNS packet size. |
udp-max-queries | integer | 0 or greater | c-ares UDP query limit. |
rotate | boolean | boolean | Enable or disable c-ares server rotation. |
Example:
{
"dns": {
"ndots": 1,
"udp-port": 53,
"tcp-port": 53,
"socket-send-buffer-size": 262144,
"socket-receive-buffer-size": 262144,
"edns-packet-size": 1232,
"udp-max-queries": 0,
"rotate": true
}
}
Avoid setting these unless you know why you need them. For most users,
servers, domain-strategy, and the timing options are enough.
Search and Source Options
| Field | Type | Validation | Meaning |
|---|---|---|---|
domains | array of strings | non-empty array; non-empty strings | Search domains passed to c-ares. |
lookups | string | only b and f; no repeated characters | Lookup source order. b means DNS, f means hosts file. |
resolvconf-path | string | non-empty string | Custom resolv.conf path. |
hosts-path | string | non-empty string | Custom hosts file path. |
sortlist | string | non-empty string | c-ares sortlist string. |
servers | string or array of strings | non-empty; array items cannot contain commas | DNS servers passed to c-ares. |
servers may be a single c-ares CSV-style string or an array. When it is an
array, WaterWall joins the entries with commas before passing them to c-ares.
Example:
{
"dns": {
"servers": [
"1.1.1.1",
"8.8.8.8"
],
"lookups": "bf",
"domains": [
"example.com"
],
"resolvconf-path": "/etc/resolv.conf",
"hosts-path": "/etc/hosts",
"sortlist": "10.0.0.0/8"
}
}
lookups values:
| Value | Meaning |
|---|---|
"b" | DNS only. |
"f" | Hosts file only. |
"bf" | Try DNS, then hosts file. |
"fb" | Try hosts file, then DNS. |
Do not repeat lookup sources. "bb", "ff", and "bfb" are rejected.
dns.flags
flags configures supported c-ares flags. It can be written in four forms.
Numeric bitmask:
{
"dns": {
"flags": 0
}
}
Single flag name:
{
"dns": {
"flags": "edns"
}
}
Array of flag names:
{
"dns": {
"flags": [
"edns",
"dns0x20"
]
}
}
Boolean object:
{
"dns": {
"flags": {
"edns": true,
"dns0x20": true,
"use-vc": false
}
}
}
In object syntax, true enables a flag and false leaves it disabled.
Supported flag names:
| Name | c-ares flag |
|---|---|
"usevc" | ARES_FLAG_USEVC |
"use-vc" | ARES_FLAG_USEVC |
"tcp" | ARES_FLAG_USEVC |
"primary" | ARES_FLAG_PRIMARY |
"igntc" | ARES_FLAG_IGNTC |
"ignore-truncated" | ARES_FLAG_IGNTC |
"norecurse" | ARES_FLAG_NORECURSE |
"no-recurse" | ARES_FLAG_NORECURSE |
"stayopen" | ARES_FLAG_STAYOPEN |
"stay-open" | ARES_FLAG_STAYOPEN |
"no-search" | ARES_FLAG_NOSEARCH |
"no-aliases" | ARES_FLAG_NOALIASES |
"nocheckresp" | ARES_FLAG_NOCHECKRESP |
"no-check-response" | ARES_FLAG_NOCHECKRESP |
"edns" | ARES_FLAG_EDNS |
"no-default-server" | ARES_FLAG_NO_DFLT_SVR |
"no-dflt-svr" | ARES_FLAG_NO_DFLT_SVR |
"dns0x20" | ARES_FLAG_DNS0x20 |
Unknown flag names stop startup. Numeric bitmasks must only contain supported c-ares flag bits.
dns.server-failover
server-failover configures c-ares server failover options.
| Field | Type | Default | Validation | Meaning |
|---|---|---|---|---|
retry-chance | integer | 10 | 0 to 65535 | c-ares retry chance value. |
retry-delay-ms | integer | 5000 | 0 or greater | Delay before retrying a failed DNS server. |
Example:
{
"dns": {
"server-failover": {
"retry-chance": 25,
"retry-delay-ms": 2000
}
}
}
If server-failover is present, it must be an object. Its fields are optional;
omitted fields keep their defaults.
Complete Structural Example
This example shows every currently documented core section and most DNS fields. Use it as a reference, not as a recommendation to tune every DNS value.
{
"log": {
"path": "log/",
"internal": {
"loglevel": "INFO",
"file": "internal.log",
"console": true
},
"core": {
"loglevel": "INFO",
"file": "core.log",
"console": true
},
"network": {
"loglevel": "INFO",
"file": "network.log",
"console": true
},
"dns": {
"loglevel": "INFO",
"file": "dns.log",
"console": true
}
},
"misc": {
"workers": 4,
"ram-profile": "server",
"mtu": 1500,
"try-enabling-bbr": true,
"libs-path": "libs/"
},
"dns": {
"domain-strategy": "prefer-ipv4",
"timeout-ms": 1000,
"max-timeout-ms": 5000,
"tries": 2,
"query-cache-max-ttl": 1800,
"ndots": 1,
"udp-port": 53,
"tcp-port": 53,
"socket-send-buffer-size": 262144,
"socket-receive-buffer-size": 262144,
"edns-packet-size": 1232,
"udp-max-queries": 0,
"flags": [
"edns",
"dns0x20"
],
"rotate": true,
"domains": [
"example.com"
],
"lookups": "bf",
"resolvconf-path": "/etc/resolv.conf",
"hosts-path": "/etc/hosts",
"sortlist": "10.0.0.0/8",
"servers": [
"1.1.1.1",
"8.8.8.8"
],
"server-failover": {
"retry-chance": 10,
"retry-delay-ms": 5000
}
},
"configs": [
"configs/server.json"
]
}
Minimal Practical Example
For most users, start smaller:
{
"log": {
"path": "logs/",
"core": {
"loglevel": "INFO",
"file": "core.log",
"console": true
},
"network": {
"loglevel": "INFO",
"file": "network.log",
"console": true
},
"dns": {
"loglevel": "INFO",
"file": "dns.log",
"console": true
},
"internal": {
"loglevel": "INFO",
"file": "internal.log",
"console": true
}
},
"misc": {
"workers": 4,
"ram-profile": "server",
"mtu": 1500,
"try-enabling-bbr": true,
"libs-path": "libs/"
},
"dns": {
"domain-strategy": "prefer-ipv4"
},
"configs": [
"configs/server.json"
]
}
DNS-Tuned Example
This example is useful when you want explicit DNS servers and faster diagnosis of resolver behavior:
{
"log": {
"path": "logs/",
"core": {
"loglevel": "INFO",
"file": "core.log",
"console": true
},
"network": {
"loglevel": "WARN",
"file": "network.log",
"console": false
},
"dns": {
"loglevel": "DEBUG",
"file": "dns.log",
"console": true
},
"internal": {
"loglevel": "INFO",
"file": "internal.log",
"console": false
}
},
"misc": {
"workers": 8,
"ram-profile": "client-larger",
"mtu": 1500,
"try-enabling-bbr": true,
"libs-path": "libs/"
},
"dns": {
"domain-strategy": "prefer-ipv4",
"servers": [
"1.1.1.1",
"8.8.8.8"
],
"timeout-ms": 1000,
"max-timeout-ms": 5000,
"tries": 2,
"query-cache-max-ttl": 600,
"lookups": "bf",
"flags": [
"edns",
"dns0x20"
],
"rotate": true,
"server-failover": {
"retry-chance": 10,
"retry-delay-ms": 5000
}
},
"configs": [
"configs/server.json",
"configs/reverse.json"
]
}
Common Startup Mistakes
| Mistake | Result | Fix |
|---|---|---|
core.json contains // comments | JSON parse error | Remove comments from core.json. |
configs is missing or empty | Startup fails | Add at least one config file path. |
A configs path is relative to the wrong directory | Config file cannot be read | Start WaterWall from the intended working directory or use correct paths. |
domain-strategy is at the root | Startup fails | Move it to dns.domain-strategy. |
dns is not an object | Startup fails | Use an object or omit dns. |
dns.flags has an unknown name | Startup fails | Use one of the documented flag names. |
misc.ram-profile has a typo | Startup fails | Use server, client, client-larger, minimal, or ultralow. |
| Too many workers | Value is reduced to 254 | Use a realistic worker count. |
What to Tune First
Start with only these values:
| Goal | Field |
|---|---|
| Run the right chains | configs |
| Control CPU usage | misc.workers |
| Choose server/client memory behavior | misc.ram-profile |
| Make logs useful | log.network.loglevel, log.dns.loglevel |
| Prefer IPv4 or IPv6 | dns.domain-strategy |
| Use known resolvers | dns.servers |
Leave advanced DNS options alone until you have a specific reason to change them. The defaults are intentionally conservative for ordinary deployments.