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 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
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
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.
All paths in core.json are passed as written. In normal deployments, relative
paths are relative to the working directory used to start WaterWall.
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.
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,
"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" when misc is present | Selects memory pool sizing. Invalid values stop startup. |
mtu | integer | 1500 when misc is present | 0 or negative falls back to 1500. |
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. |
When misc is present and non-empty, omitted fields use the defaults above. If
the whole block is omitted, the current parser explicitly falls back to CPU core
count for workers and "libs/" for libs-path. For predictable deployments,
keep the whole misc block explicit.
workers
workers controls how many worker threads WaterWall creates.
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 |
Invalid numeric or string values stop startup.
mtu
mtu is the global MTU value exposed to tunnels that need an MTU default.
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 only rejects 0 and negative values. Keep the value in a realistic
MTU range; do not use oversized values.
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 changing system TCP settings at startup.
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.