A speed-first DNS tunnel for censored, lossy, and unstable networks.
English · فارسی · Русский · 简体中文 · Español
Download · Engineering notes · Configuration presets · Telegram
Important
CottenDNS exists for networks where ordinary VPNs and proxies are blocked but a usable DNS path remains. DNS tunneling is inherently slower than a normal VPN. The goal is maximum usable speed and continuity under censorship—not unrealistic clean-network throughput claims.
| I want to… | Go to |
|---|---|
| Deploy a new tunnel | Complete setup guide |
| Add IPv6 and automatic fallback | IPv4 and IPv6 setup |
| Harden a public server | Flood and abuse protection |
| Run the server with Docker | Docker server installation |
| Understand the adaptive transport system | Automatic behavior |
| Pick a speed or survival profile | Choose a profile |
| Check server load and tunnel health | Monitoring |
| Embed the engine in Android | Android and engine embedding |
| Diagnose a failed or slow connection | Troubleshooting |
| Review every configuration option | Client and server samples |
| Component | Purpose | Normal default |
|---|---|---|
| Server | Authoritative DNS endpoint and tunnel egress | Linux VPS, UDP + TCP port 53 |
| Client | Resolver scanning, path selection, reliability, local proxy | SOCKS5 on 127.0.0.1:18000 |
| Carrier | Moves encrypted frames through DNS | Per-resolver auto between UDP/53 and TCP/53 |
| Optional carriers | Survive networks that fingerprint plain DNS | User-enabled DoT or DoH |
| Reliability | Repairs unstable delivery | ARQ, adaptive duplication, FEC, replay |
| Control plane | Keeps traffic on the best measured path | Unified per-resolver path controller |
CottenDNS stands on the work of two projects and their maintainers:
- MasterDnsVPN by Amin Mahmoudi established the low-overhead DNS tunnel, custom ARQ, resolver balancing, session multiplexing, and harsh-network design used by this project.
- StormDNS by NullRoute1970 continued that work and is the direct upstream lineage from which CottenDNS was derived.
- CottenDNS, maintained by tajirax, extends that foundation while retaining compatibility with MasterDNS/StormDNS clients.
Their work deserves full credit. CottenDNS is an independent derivative, not a claim that the underlying architecture began here. All three projects preserve the MIT license lineage; see LICENSE.
CottenDNS carries application traffic inside DNS queries and responses:
Application
│
▼
Local SOCKS5 / TCP listener
│
▼
CottenDNS client
│
├── UDP/53 ─────┐
├── TCP/53 ─────┤
├── DoT ────────┤──► DNS resolver ──► CottenDNS server ──► Internet
└── DoH ────────┘
The client splits streams into DNS-sized frames, encrypts and optionally compresses them, selects a resolver/transport path, and repairs loss with ARQ and optional Reed–Solomon FEC. The server reconstructs the streams and opens the requested connection directly, through an upstream SOCKS5 server, or toward a fixed TCP target.
The built-in SOCKS5 server supports normal TCP proxying and generic UDP associations. A local DNS listener/cache is optional.
| Network problem | CottenDNS response |
|---|---|
| UDP is fast but sometimes poisoned or throttled | Keep viable UDP; move only the affected resolver to a better transport |
| Resolver paths behave differently | Score each (resolver, transport) independently |
| Upload and download quality differ | Maintain separate directional delivery, failure, RTT, and confidence evidence |
| Small DNS MTUs waste capacity | Discover MTU per path and route each packet only where it fits |
| Packet loss is severe | ARQ, ACK/NACK, immediate replay, adaptive duplication, FEC, and Super-FEC |
| A forged answer arrives unusually fast | Treat it as an early warning and race one authenticated alternate |
| A path dies with frames in flight | Replay unacknowledged frames without recreating the session |
| Extra reliability traffic causes congestion | Coordinate duplication, FEC, replay, and exploration under one budget |
| Several paths are equally good | Stripe bulk packets across mature comparable paths without making copies |
| DPI fingerprints repetitive DNS | Rotate record types, reshape QNAME labels, randomize IDs/cookies, and optionally use DoT/DoH |
| Server load spikes | Bounded queues, admission checks, connection budgets, and work-conserving fairness |
The recommended defaults enable the smart client controller:
CONFIG_PRESET = "speed"
RESOLVER_TRANSPORT = "auto"
PATH_CONTROLLER_MODE = "unified"
COMPARABLE_PATH_STRIPING = trueWith these settings the client automatically:
- tries UDP and keeps it while it remains useful;
- compares UDP and TCP/53 separately for every resolver;
- switches only the failing or materially slower path;
- uses packet size, MTU, RTT, loss, direction, and confidence when routing;
- keeps alternate paths warm with a congestion-aware background budget;
- avoids stacking FEC, exploration, and unnecessary duplication;
- races a validated alternate after poison evidence;
- replays in-flight frames before waiting for the full ARQ timeout; and
- falls back to connectivity when the fastest path cannot deliver.
auto does not silently enable DoT or DoH. Those transports remain explicit user choices because some censored networks block them:
RESOLVER_TRANSPORT = "dot" # or "doh"If an encrypted resolver transport fails, its survival chain can return to UDP/53 and TCP/53. You may also override individual resolvers:
RESOLVER_TRANSPORT_PATHS = { "1.1.1.1" = "doh", "8.8.8.8" = "auto", "9.9.9.9" = "tcp" }The server feeds UDP, TCP/53, DoT, and DoH into the same native session engine. It does not require a separate session protocol for each carrier.
With the default:
ENCRYPTION_AUTO_DETECT = truethe server automatically accepts keyed client encryption methods 1–5:
| ID | Method | Authentication |
|---|---|---|
1 |
XOR | Legacy, unauthenticated |
2 |
ChaCha20 | Legacy stream cipher, unauthenticated |
3 |
AES-128-GCM | Authenticated |
4 |
AES-192-GCM | Authenticated |
5 |
AES-256-GCM | Authenticated |
AES-GCM candidates are always checked before legacy decoders. Legacy candidates must additionally match session or strict pre-session MTU semantics. Plaintext method 0 is accepted only when the server is explicitly configured for plaintext.
Different clients may therefore select different supported methods, transports, resolver paths, MTUs, compression, and reliability settings without reconfiguring the server. The server still enforces its configured safety ceilings.
The normal deployment has three parts:
[Client device] ──► [recursive DNS resolvers] ──► [delegated subdomain on VPS] ──► [Internet]
You need:
- a domain whose DNS records you can edit;
- a Linux VPS with a public IPv4 address and root or
sudoaccess; - inbound UDP/53 and TCP/53 allowed by both the VPS firewall and the provider firewall;
- a client device running Windows, Linux, macOS, or an application embedding the engine; and
- at least a few resolvers that can reach your delegated authoritative subdomain.
Caution
Port 53 may already be occupied by systemd-resolved, BIND, dnsmasq, or a hosting control panel. The installer detects common conflicts, but check the host before using a production DNS server. Keep TCP/22 or your management port open so firewall changes cannot lock you out.
Choose two names:
- nameserver host:
ns.example.com - tunnel subdomain:
v.example.com
Create these records at the authoritative DNS provider for example.com:
ns.example.com. 300 IN A 203.0.113.10
v.example.com. 300 IN NS ns.example.com.Replace 203.0.113.10 with the VPS public IP. The first record points the nameserver name to the VPS; the second delegates the tunnel subdomain to that nameserver. Do not proxy either record through a CDN. A short tunnel name is preferable because every extra domain byte reduces usable DNS payload.
Wait for propagation, then verify from a machine with dig:
dig +short A ns.example.com
dig +short NS v.example.comExpected:
203.0.113.10
ns.example.com.
If the NS result is missing, stop here and fix delegation. Client tuning cannot compensate for incorrect DNS records.
IPv6 is optional. The recommended arrangement keeps IPv4 as the preferred path and uses IPv6 when the user supplies IPv6 resolvers and IPv4 becomes unhealthy. The VPS must have a routed public IPv6 address; a link-local address is not enough.
Add an AAAA glue record beside the existing A record:
ns.example.com. 300 IN A 203.0.113.10
ns.example.com. 300 IN AAAA 2001:db8:100::10
v.example.com. 300 IN NS ns.example.com.Allow inbound UDP/53 and TCP/53 for both address families in the provider and
host firewalls. Then enable the IPv6 listeners in server_config.toml:
# On normal Linux dual-stack hosts this accepts UDP DNS over IPv6 and IPv4.
UDP_HOST = "::"
TCP_LISTENER_ENABLED = true
TCP_IPV6_ENABLED = true
TCP_IPV6_HOST = "::"
# Optional: carry response data in dense AAAA records as well as using IPv6 as
# the network carrier. These are independent features.
AAAA_RECORD_DATA_DELIVERY = trueRestart and verify the actual sockets. Do not assume the provider enabled IPv6
or that a platform treats [::] as dual-stack:
sudo systemctl restart cottendns
sudo ss -lnup | grep ':53 '
sudo ss -lntp | grep ':53 '
dig @2001:db8:100::10 v.example.com SOA +tcpList reachable recursive resolvers on the client. Brackets are required when an IPv6 literal includes a port:
1.1.1.1:53
8.8.8.8:53
[2606:4700:4700::1111]:53
[2001:4860:4860::8888]:53
Use automatic family selection in client_config.toml:
RESOLVER_IP_MODE = "auto"
RESOLVER_TRANSPORT = "auto"auto prefers healthy IPv4 paths and activates supplied IPv6 paths when IPv4
cannot establish or maintain the session. Use ipv4, ipv6, or dual only
when you deliberately want to restrict or concurrently operate both families.
Confirm that port 53 is not unexpectedly occupied:
sudo ss -lntup | grep ':53 'Also allow UDP/53 and TCP/53 in the VPS provider’s network firewall or security group. The installer handles common host firewalls, but it cannot modify the provider control panel.
Create a stable installation directory and run the latest installer:
sudo install -d -m 0755 /opt/cottendns
cd /opt/cottendns
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_linux_install.sh | sudo bashWhen prompted, enter the delegated tunnel domain—for example v.example.com. The installer:
- downloads the matching server release;
- prepares
server_config.toml; - generates or preserves
encrypt_key.txt; - opens UDP and TCP port 53 in supported host firewalls;
- creates and starts
cottendns.service; and - verifies the local
/healthzendpoint.
Important files:
/opt/cottendns/server_config.toml
/opt/cottendns/encrypt_key.txt
Read the generated key and transfer it through a secure channel:
sudo cat /opt/cottendns/encrypt_key.txtNever post this key in an issue, screenshot, resolver list, or public configuration.
Run all four checks:
systemctl is-active cottendns
systemctl status cottendns --no-pager
curl -fsS http://127.0.0.1:9090/healthz
sudo ss -lnup | grep ':53 '
sudo ss -lntp | grep ':53 'You should see an active service, a healthy response, and listeners on port 53 for both UDP and TCP. If not:
journalctl -u cottendns -n 100 --no-pagerThe metrics listener intentionally binds to localhost. Do not expose port 9090 publicly just to make these checks work.
CottenDNS is an Internet-facing authoritative DNS service, so application
limits complement—but do not replace—provider DDoS filtering. Keep the VPS
provider firewall limited to the ports you use (normally UDP/53, TCP/53, and a
restricted management port). Never expose the local metrics port 9090.
Start with authenticated encryption. AES-GCM methods 3–5 reject unauthenticated
frames before session work; plaintext method 0 should never be used on a public
server. If every client is under your control, set method 5 on both sides and
disable auto-detection so legacy unauthenticated codecs are not accepted. Keep
auto-detection only when compatibility with mixed clients is required. Protect
encrypt_key.txt, because a user with the shared key is an authorized tunnel
client and can generate valid workload.
The default server sample enables layered resource ceilings:
| Threat | Protection |
|---|---|
| Random UDP/DNS packet flood | Cheap DNS/domain/decryption admission before bounded worker queues |
| UDP queue memory exhaustion | MAX_CONCURRENT_REQUESTS, MAX_INGRESS_QUEUE_BYTES, and MAX_PACKET_SIZE |
| Session and stream creation | MAX_ACTIVE_SESSIONS and MAX_STREAMS_PER_SESSION |
| TCP/53, DoT, or DoH idle connection flood | TCP_MAX_CONNS, TCP_MAX_CONNS_PER_IP, idle deadlines, and a reserved plain-TCP budget |
| DoH request/body flood | DOH_MAX_INFLIGHT, DOH_MAX_INFLIGHT_BYTES, request-rate and burst limits |
| IPv6 privacy-address rotation | Per-client TCP and DoH identities are grouped by IPv6 /64; IPv4 remains per address |
| Forged forwarding headers | DoH honors headers only from DOH_TRUSTED_PROXY_CIDRS and walks proxy chains from the trusted side |
| Address/cardinality memory attack | DoH limiter and invalid-cookie tracking state are hard-bounded instead of growing indefinitely |
Recommended public-server baseline:
DATA_ENCRYPTION_METHOD = 5
ENCRYPTION_AUTO_DETECT = false
MAX_CONCURRENT_REQUESTS = 16384
MAX_INGRESS_QUEUE_BYTES = 67108864
MAX_PACKET_SIZE = 4096
MAX_ACTIVE_SESSIONS = 2048
MAX_STREAMS_PER_SESSION = 4096
TCP_MAX_CONNS = 2048
TCP_MAX_CONNS_PER_IP = 128
TCP_READ_IDLE_TIMEOUT_SECONDS = 30.0
DOH_MAX_INFLIGHT = 256
DOH_MAX_INFLIGHT_BYTES = 67108864
DOH_REQUESTS_PER_SECOND_PER_IP = 4096
DOH_REQUEST_BURST_PER_IP = 8192Only configure DOH_TRUSTED_PROXY_CIDRS when DoH is actually behind a reverse
proxy, and list only the exact proxy addresses or internal proxy subnets. An
overly broad trusted range lets arbitrary hosts claim forwarded identities.
UDP DNS commonly arrives from shared recursive resolvers, and source addresses can be spoofed in direct attacks. CottenDNS therefore protects UDP globally with authenticated early admission, byte/count-bounded queues, bounded sessions, and load shedding instead of applying a naive per-source limit that could let one attacker block an entire public resolver. For volumetric floods that fill the network link before the process sees packets, enable the VPS provider's DDoS protection or an upstream DNS-aware firewall.
Monitor /metrics over localhost and investigate sustained increases in queue
drops, admission rejects, session-table pressure, connection rejects, or DoH
rate-limit responses. Raise limits only after confirming CPU, memory, file
descriptors, and network capacity have headroom.
Docker is an alternative to the systemd server installation in Steps 2 and 3.
The supported image is published from this repository to
ghcr.io/tajirax/cottendns-server for AMD64 and ARM64 Linux. It runs the
CottenDNS process as a non-root user, persists the configuration and encryption
key under /data, and includes the same local health endpoint as the native
service.
- A Linux VPS with Docker Engine and the Docker Compose plugin.
- The DNS delegation from Step 1.
- Free UDP/53 and TCP/53 ports on the host.
- Inbound UDP/53 and TCP/53 allowed in both the provider firewall and the host firewall.
Check for an existing DNS service before starting:
sudo ss -lntup | grep ':53 '
docker --version
docker compose versionIf BIND, dnsmasq, systemd-resolved, or another container owns port 53, stop or
reconfigure that service first. The container uses host networking so DNS
traffic reaches CottenDNS without Docker NAT overhead.
Replace v.example.com with the delegated tunnel domain:
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_docker_install.sh \
| sudo sh -s -- --domain v.example.comThe installer creates /opt/cottendns-docker, downloads the repository-owned
Compose file, pulls the current image, starts the server, and waits until the
container health check passes. Persistent files are stored here:
/opt/cottendns-docker/data/server_config.toml
/opt/cottendns-docker/data/encrypt_key.txt
/opt/cottendns-docker/.env
Read the generated key and transfer it securely to the client:
sudo cat /opt/cottendns-docker/data/encrypt_key.txtVerify the deployment:
cd /opt/cottendns-docker
sudo docker compose ps
sudo docker compose logs --tail=100 server
curl -fsS http://127.0.0.1:9090/healthz
sudo ss -lnup | grep ':53 '
sudo ss -lntp | grep ':53 'The metrics endpoint stays bound to 127.0.0.1:9090. Do not publish it to the
Internet. For detailed health and Prometheus data, use the commands in
Monitoring.
Edit non-domain settings in the persistent server configuration, then restart the server process:
sudoedit /opt/cottendns-docker/data/server_config.toml
cd /opt/cottendns-docker
sudo docker compose restart server
sudo docker compose logs --tail=100 serverThe domain is controlled by COTTENDNS_DOMAIN in
/opt/cottendns-docker/.env. After changing it, recreate the container so
Compose loads the new environment:
sudoedit /opt/cottendns-docker/.env
cd /opt/cottendns-docker
sudo docker compose up -d --force-recreateThe entrypoint writes this value into server_config.toml at startup, so do not
set a different DOMAIN value in the TOML file.
Upgrade to the newest published image without replacing the active configuration or encryption key:
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_docker_install.sh \
| sudo sh -s -- --upgradeBack up persistent state before an upgrade:
sudo cp -a /opt/cottendns-docker/data \
"/opt/cottendns-docker/data.backup.$(date +%Y%m%d-%H%M%S)"For a reproducible deployment, set COTTENDNS_IMAGE_TAG in
/opt/cottendns-docker/.env to a release tag instead of latest:
COTTENDNS_DOMAIN=v.example.com
COTTENDNS_IMAGE_TAG=vYYYY.MM.DD.HHMMSS-commithashApply a pinned version—or roll back by selecting the previous known-good tag—with:
cd /opt/cottendns-docker
sudo docker compose pull
sudo docker compose up -d --remove-orphans
curl -fsS http://127.0.0.1:9090/healthzStop and remove only the container and Compose-managed resources:
cd /opt/cottendns-docker
sudo docker compose downThe bind-mounted data directory remains on disk. Preserve
server_config.toml and encrypt_key.txt; deleting or replacing the key
disconnects clients that use it.
Open Releases and choose the archive matching the client machine:
| Platform | Typical archive |
|---|---|
| Windows 64-bit | CottenDns_Client_Windows_AMD64.zip |
| Linux x86-64 | CottenDns_Client_Linux_AMD64.tar.gz or .zip |
| Linux ARM64 | CottenDns_Client_Linux_ARM64.tar.gz or .zip |
| macOS Apple Silicon | CottenDns_Client_Darwin_ARM64.tar.gz or .zip |
| macOS Intel | CottenDns_Client_Darwin_AMD64.tar.gz or .zip |
Keep the executable, client_config.toml, and client_resolvers.txt in the same directory. Configuration paths are resolved relative to the executable unless an explicit path is provided.
Edit only the essential values first:
DOMAINS = ["v.example.com"]
ENCRYPTION_KEY = "paste-the-server-key-here"
DATA_ENCRYPTION_METHOD = 3
PROTOCOL_TYPE = "SOCKS5"
LISTEN_IP = "127.0.0.1"
LISTEN_PORT = 18000
CONFIG_PRESET = "speed"
STARTUP_MODE = "resolvers"
RESOLVER_TRANSPORT = "auto"
PATH_CONTROLLER_MODE = "unified"
COMPARABLE_PATH_STRIPING = trueThe domain must exactly match the server. Method 3 is authenticated AES-128-GCM and is the recommended starting point. The dynamic server accepts keyed methods 1–5 by default, so other clients may use a different supported method without changing the server.
Put one resolver IP per line in client_resolvers.txt:
# Comments and blank lines are allowed.
1.1.1.1
8.8.8.8
9.9.9.9
Start with a small, diverse set. A huge list of poor resolvers increases scan time and does not guarantee more throughput.
Run from a terminal so you can see scan results.
Linux/macOS:
chmod +x ./CottenDns_Client_*
./CottenDns_Client_Linux_AMD64_vVERSION --config ./client_config.tomlWindows PowerShell:
.\CottenDns_Client_Windows_AMD64_vVERSION.exe --config .\client_config.tomlDuring the first run, CottenDNS measures resolver reachability, MTU, latency, loss, and available UDP/TCP paths. A resolver does not enter the active pool merely because it answers ordinary DNS; it must pass tunnel validation.
For a scan without starting the local proxy:
./CottenDns_Client_Linux_AMD64_vVERSION --config ./client_config.toml --scan-onlyKeep STARTUP_MODE = "resolvers" on every launch. Resolver reachability,
transport quality, poisoning, and MTU can change between restarts, so CottenDNS
always validates the complete current list. FAST_CONNECT = true releases a
small validated starter pool early and continues scanning the remaining
resolvers in the background without trusting results from a previous process.
Configure an application to use:
Type: SOCKS5
Host: 127.0.0.1
Port: 18000
Or test from a terminal:
curl --proxy socks5h://127.0.0.1:18000 https://example.com/Use socks5h, not socks5, when you want hostname resolution to occur through the proxy.
The Linux client archive includes an offline systemd installer. Configure the files first, then run it from the extracted directory:
sudo bash client_linux_install.sh
systemctl status cottendns-client --no-pager
journalctl -u cottendns-client -fUpgrade the server in place:
cd /opt/cottendns
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_linux_install.sh | sudo bash -s -- --upgradeThe upgrade path preserves the active configuration and encryption key and rolls back if the new service does not become healthy. Still keep an external backup:
sudo cp /opt/cottendns/server_config.toml /opt/cottendns/server_config.toml.manual-backup
sudo cp /opt/cottendns/encrypt_key.txt /opt/cottendns/encrypt_key.txt.manual-backupFor desktop clients, extract the new release separately, copy your configuration/key and resolver file, then start the new binary. Keep the previous folder until the replacement has completed a scan and passed a SOCKS test.
Server installer: pinned, offline, and removal operations
Install a specific release:
cd /opt/cottendns
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_linux_install.sh | sudo bash -s -- --version vYYYY.MM.DD.HHMMSS-commithashInstall a locally built server without downloading:
python build.py
sudo bash server_linux_install.sh --localShow installer options:
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_linux_install.sh | bash -s -- --helpUninstall the systemd service and files managed by the installer:
cd /opt/cottendns
curl -fsSL https://raw.githubusercontent.com/TaJirax/cottenDNS/main/server_linux_install.sh | sudo bash -s -- --uninstallBack up server_config.toml and encrypt_key.txt before uninstalling if the deployment may be restored later.
| Profile | Use it when |
|---|---|
speed |
Default choice; viable DNS with maximum useful throughput |
survival |
Heavy loss, resolver instability, or aggressive censorship |
tcp-survival |
UDP/53 is consistently unusable and TCP/53 survives |
iran, china, russia, venezuela, cuba |
Client-side starting points for common regional constraints |
low-bandwidth |
Upload capacity and resolver query budgets are extremely limited |
Profiles are starting points, not hard-coded country detection. Real resolver behavior varies by ISP, city, time, and censorship event. Explicit TOML values always override a preset. See CONFIG_PRESETS.md.
| Setting | Best use | Important behavior |
|---|---|---|
auto |
Recommended for almost everyone | Tries UDP first and compares UDP/TCP per resolver; it does not enable DoT/DoH |
udp |
Debugging or a known clean UDP-only environment | Pins every unoverridden resolver to UDP and disables automatic TCP selection |
tcp |
Networks where UDP/53 is consistently broken | Uses persistent DNS-over-TCP connections |
dot |
Plain DNS is fingerprinted but port 853 works | Requires the server DoT listener and correct TLS trust settings |
doh |
HTTPS-shaped DNS is the only reliable carrier | Requires the server DoH listener, route, and correct TLS trust settings |
Prefer auto over forcing the entire pool to one transport. Per-resolver policy keeps fast UDP where it works while allowing only a damaged path to use TCP:
RESOLVER_TRANSPORT = "auto"
RESOLVER_TRANSPORT_PATHS = { "1.1.1.1" = "udp", "8.8.8.8" = "auto", "9.9.9.9" = "tcp" }DoT and DoH are deliberate deployments, not speed toggles. Read the TLS, certificate pinning, coexistence, and listener comments in server_config.toml.simple and client_config.toml.simple before enabling them.
The default metrics listener is local-only. Plain health:
curl -s http://127.0.0.1:9090/healthzDetailed JSON:
curl -s 'http://127.0.0.1:9090/healthz?details=1' | jqPrometheus metrics:
curl -s http://127.0.0.1:9090/metricsThe detailed view includes build/uptime, sessions, streams, listener state, queue pressure, drops, codec acceptance, generic UDP, DNS upstream health, Go runtime, CPU-related runtime counters, and memory data. It intentionally excludes keys, credentials, queried domains, client addresses, and payloads.
Read the signals together:
| Signal | Healthy pattern | Investigate when |
|---|---|---|
| Service and listeners | healthz succeeds; UDP/TCP listeners are up |
Listener is down or restart count rises |
| Sessions and streams | Change with real client use | Sessions grow without cleanup or streams never open |
| Queue pressure | Usually low with short bursts | Sustained saturation, rejects, or drops increase |
| Codec acceptance | Authenticated/expected methods dominate | Invalid or rejected traffic grows sharply |
| Generic UDP | Active only when applications request it | Associations accumulate or stale drops rise |
| Go memory | May grow, then stabilize and reuse | Resident memory rises continuously with no workload growth |
| CPU | Bursts during traffic and FEC work | High CPU continues while traffic is low |
healthz reports server condition; it does not prove that a particular ISP resolver can carry the tunnel. Resolver viability, MTU, and path quality must be checked from the client’s network.
For operating-system process usage:
pid=$(systemctl show -p MainPID --value cottendns)
ps -p "$pid" -o pid,%cpu,%mem,rss,vsz,etime,cmd
top -p "$pid"The Go networking engine is kept in this repository. Android applications can pin CottenDNS as a Git submodule or CI dependency and build the engine without copying it into the app repository.
Known-good engine commit:
8eea49d1267b1a41441c25c75e69e6f12ad0a11f
The engine has been cross-compiled with CGO disabled for Android arm64, armv7, amd64, and 386. The Android wrapper should preserve the recommended defaults or pass equivalent TOML values.
Example submodule update:
git submodule update --init --recursive
cd path/to/cottenDNS
git fetch origin
git checkout 8eea49d1267b1a41441c25c75e69e6f12ad0a11f
cd ..
git add path/to/cottenDNS
git commit -m "Update CottenDNS engine"- New client + new server: complete feature set.
- Old client + new server: supported, including MasterDNS/StormDNS-compatible clients.
- New client + old server: most client-only routing improvements work, but current server hardening, monitoring, listeners, and policy features are unavailable.
PATH_CONTROLLER_MODE = "legacy"restores the earlier client selector without changing the wire protocol.
- Prefer AES-GCM method
3,4, or5for authenticated encryption. - XOR and ChaCha20 method
2are retained for legacy compatibility; they do not authenticate frames. - DoT/DoH protects and disguises the client-to-resolver hop, not the resolver-to-authoritative-server hop.
- DNS volume and timing can still reveal tunnel-like behavior. No plaintext DNS tunnel is invisible to a capable observer.
- Keep
/healthzand/metricsbound to localhost unless protected by firewall or an authenticated reverse proxy. - Protect
encrypt_key.txtand never publish it. - Use this software only where you are authorized to do so.
Start with the layer that failed:
| Symptom | First checks |
|---|---|
| Server will not start | journalctl, port-53 conflicts, config syntax, permissions |
| Server is healthy but every scan fails | A/NS delegation, provider firewall, matching domain/key |
| Some resolvers pass and others fail | Expected resolver filtering; keep only validated paths |
| SOCKS listener opens but browsing fails | Client logs, active streams, proxy hostname mode, server egress |
| Resolver conditions changed after restart | No action: every launch performs fresh validation |
| Fast briefly, then stalls | Queue pressure, MTU changes, loss, resolver throttling |
- Verify both records:
dig +short A ns.example.comanddig +short NS v.example.com. - Confirm the service is healthy and both UDP/TCP listeners exist with
ss. - Confirm UDP and TCP port 53 are open in the provider firewall, not only the VPS firewall.
- Confirm client and server use the exact same domain and encryption key.
- Run the client once with
STARTUP_MODE = "resolvers"and watch the scan output. - Try a shorter tunnel domain and conservative MTU bounds.
- Test a small set of known resolver IPs instead of a large unverified list.
- Use the
survivalprofile before forcing every resolver to TCP.
Keep RESOLVER_TRANSPORT = "auto". The client should retain viable UDP and move only a resolver whose UDP path fails or becomes materially worse. Every restart performs fresh resolver and path validation.
- Check queue pressure and drops in
/healthz?details=1. - Compare the client’s delivered RTT and loss per resolver/transport, not ordinary DNS ping alone.
- Avoid increasing duplication blindly; extra copies consume the same constrained link.
- Use
speedon clean paths andsurvivalonly when loss justifies it. - Keep several genuinely independent, validated resolvers; remove consistently poor paths.
- Reduce MTU if large queries are being fragmented or discarded.
- Check whether server CPU is saturated during FEC work and whether memory is stable.
- Remember that already-compressed HTTPS/video/archive traffic gains little from tunnel compression.
Confirm ENCRYPTION_AUTO_DETECT = true, use the same encryption key, and do not enable server ceilings that reject the legacy client’s requested values.
| Document | Purpose |
|---|---|
| Configuration presets | Recommended speed/survival profiles and regional starting points |
| Engineering changes | Protocol decisions, failure handling, tests, and measured results |
| Client sample | Complete client configuration reference |
| Server sample | Complete server configuration reference |
| Benchmark guide | Local client/server integration benchmark |
Requirements: Go 1.25 or compatible current Go toolchain.
go build ./cmd/client
go build ./cmd/server
go test ./...
go vet ./...Cross-platform packages:
python build.pyHostile-network regression suite:
powershell -ExecutionPolicy Bypass -File scripts/test-hostile-network.ps1 -Count 20 -FullRaceCottenDNS is maintained by tajirax and distributed under the MIT License.
Special thanks and full historical credit:
- Amin Mahmoudi — MasterDnsVPN, the original architecture and project foundation.
- NullRoute1970 — StormDNS, the direct upstream derivative and continued engineering lineage.
- All contributors, testers, resolver operators, and users reporting behavior from censored networks.
Project links: issues · pull requests · releases
