Skip to main content

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": []
}
FieldTypeRequiredPurpose
configsarray of stringsyesNode config files to parse and run.
logobjectnoLogger paths, files, log levels, and console output.
miscobjectnoWorker count, memory profile, MTU, Linux BBR tuning, and library path.
dnsobjectnoShared 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:

RuleBehavior
Missing configsStartup fails.
Empty arrayStartup fails.
No string entriesStartup fails.
Relative pathsResolved relative to the process working directory.
Multiple filesLoaded 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.

LoggerPurpose
internalLow-level runtime and internal diagnostics.
coreStartup, config parsing, and manager-level logs.
networkNetwork and tunnel runtime logs.
dnsShared async DNS resolver logs.

Each logger accepts the same fields:

FieldTypeDefaultNotes
loglevelstring"INFO"Minimum level written by that logger.
filestringlogger-specificFile name appended to log.path.
consolebooleantrueAlso 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:

FieldDefault
log.path"log/"
log.internal.loglevel"INFO"
log.internal.file"internal.log"
log.internal.consoletrue
log.core.loglevel"INFO"
log.core.file"core.log"
log.core.consoletrue
log.network.loglevel"INFO"
log.network.file"network.log"
log.network.consoletrue
log.dns.loglevel"INFO"
log.dns.file"dns.log"
log.dns.consoletrue

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:

SituationRecommendation
First setup or debuggingKeep core, network, and dns console output enabled.
Production with many connectionsUse INFO or WARN for network; avoid very noisy levels unless debugging.
DNS troubleshootingTemporarily set log.dns.loglevel to "DEBUG".
Long-running serviceKeep 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/"
}
}
FieldTypeDefaultValidation and behavior
workersintegerCPU core count0 or negative falls back to CPU core count. Values above 254 are reduced to 254.
ram-profilestring or integer"server" when misc is presentSelects memory pool sizing. Invalid values stop startup.
mtuinteger1500 when misc is present0 or negative falls back to 1500.
try-enabling-bbrbooleantrueLinux-only best-effort attempt to enable TCP BBR if the running kernel reports BBR support.
libs-pathstring"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:

DeploymentStarting point
Small client or test instance1 or 2
General VPS serverCPU core count
Heavy traffic serverCPU 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:

ValueInternal profileTypical use
"server"L2 memory profileDefault server-side profile with larger pools.
"client"M1 memory profileGeneric client-side profile.
"client-larger"M2 memory profileLarger client-side profile.
"minimal"S1 memory profileMinimal memory profile.
"ultralow"S1 memory profileAlias of "minimal".

The string value is lowercased by the parser before matching. Prefer string values in new configs.

Legacy integer values are also accepted:

ValueProfile
0 or 1S1 memory profile
2S2 memory profile
3M1 memory profile
4M2 memory profile
5L1 memory profile
6L2 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:

SettingDefault
Resolver optionsWaterWall's c-ares defaults
dns.domain-strategy"prefer-ipv4"
timeout-ms1000
max-timeout-ms5000
tries2
query-cache-max-ttl1800
server-failover.retry-chance10
server-failover.retry-delay-ms5000

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:

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

ValueStrategy
0accept-dns-returned-order
1prefer-ipv4
2prefer-ipv6
3only-ipv4
4only-ipv6

Example:

{
"dns": {
"domain-strategy": "prefer-ipv4"
}
}

Practical guidance:

EnvironmentStrategy
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

FieldTypeDefaultValidationMeaning
timeout-msinteger1000greater than 0Initial DNS query timeout in milliseconds.
max-timeout-msinteger5000greater than 0Maximum DNS query timeout after retries.
triesinteger2greater than 0Number of query attempts.
query-cache-max-ttlinteger18000 or greaterMaximum 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.

FieldTypeValidationMeaning
ndotsinteger0 to 15Number of dots required before c-ares tries a name as absolute.
udp-portinteger1 to 65535DNS UDP server port.
tcp-portinteger1 to 65535DNS TCP server port.
socket-send-buffer-sizeintegergreater than 0DNS socket send buffer size.
socket-receive-buffer-sizeintegergreater than 0DNS socket receive buffer size.
edns-packet-sizeinteger1 to 65535EDNS packet size.
udp-max-queriesinteger0 or greaterc-ares UDP query limit.
rotatebooleanbooleanEnable 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

FieldTypeValidationMeaning
domainsarray of stringsnon-empty array; non-empty stringsSearch domains passed to c-ares.
lookupsstringonly b and f; no repeated charactersLookup source order. b means DNS, f means hosts file.
resolvconf-pathstringnon-empty stringCustom resolv.conf path.
hosts-pathstringnon-empty stringCustom hosts file path.
sortliststringnon-empty stringc-ares sortlist string.
serversstring or array of stringsnon-empty; array items cannot contain commasDNS 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:

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

Namec-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.

FieldTypeDefaultValidationMeaning
retry-chanceinteger100 to 65535c-ares retry chance value.
retry-delay-msinteger50000 or greaterDelay 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

MistakeResultFix
core.json contains // commentsJSON parse errorRemove comments from core.json.
configs is missing or emptyStartup failsAdd at least one config file path.
A configs path is relative to the wrong directoryConfig file cannot be readStart WaterWall from the intended working directory or use correct paths.
domain-strategy is at the rootStartup failsMove it to dns.domain-strategy.
dns is not an objectStartup failsUse an object or omit dns.
dns.flags has an unknown nameStartup failsUse one of the documented flag names.
misc.ram-profile has a typoStartup failsUse server, client, client-larger, minimal, or ultralow.
Too many workersValue is reduced to 254Use a realistic worker count.

What to Tune First

Start with only these values:

GoalField
Run the right chainsconfigs
Control CPU usagemisc.workers
Choose server/client memory behaviormisc.ram-profile
Make logs usefullog.network.loglevel, log.dns.loglevel
Prefer IPv4 or IPv6dns.domain-strategy
Use known resolversdns.servers

Leave advanced DNS options alone until you have a specific reason to change them. The defaults are intentionally conservative for ordinary deployments.