This document describes the testing strategy for dnstm.
dnstm uses a three-tier testing approach:
| Level | Purpose | Dependencies | Speed |
|---|---|---|---|
| Unit | Test individual functions | None | Fast (~1s) |
| Integration | Test CLI commands with mocked systemd | None | Fast (~1s) |
| E2E | Test actual tunnel connectivity | Auto-downloaded | Slow (~15s+) |
*_test.go- Test files live alongside the code they testinternal/testutil/- Shared test utilitiestests/integration/- Integration tests for CLI commandstests/e2e/- End-to-end tests with real binaries
| Command | What it runs | Requirements |
|---|---|---|
make test |
Unit + Integration | None |
make test-unit |
Unit tests only | None |
make test-integration |
Integration tests only | None |
make test-e2e |
E2E tests | Internet (first run) |
make test-all |
Unit + Integration + E2E | Internet (first run) |
make test-coverage |
Unit + Integration with coverage | None |
make test-real |
All tests with real systemd | Root |
make test-unit
# Or specific packages
go test -v ./internal/config/...
go test -v ./internal/keys/...make test-integrationUses MockSystemdManager - no root privileges or external binaries required.
make test-e2eE2E tests automatically download required binaries on first run to tests/.testbin/.
Binaries downloaded:
dnstt-client,dnstt-serverfrom net2share/dnsttslipstream-client,slipstream-serverfrom net2share/slipstream-rust-buildsslocal,ssserverfrom shadowsocks-rust
On first E2E test run, binaries are downloaded to tests/.testbin/. Subsequent runs use cached binaries.
Use environment variables to override binary paths:
Server binaries (used in production and tests):
| Variable | Binary |
|---|---|
DNSTM_DNSTT_SERVER_PATH |
dnstt-server |
DNSTM_SLIPSTREAM_SERVER_PATH |
slipstream-server |
DNSTM_SSSERVER_PATH |
ssserver |
DNSTM_MICROSOCKS_PATH |
microsocks |
Client binaries (test only):
| Variable | Binary |
|---|---|
DNSTM_TEST_DNSTT_CLIENT_PATH |
dnstt-client |
DNSTM_TEST_SLIPSTREAM_CLIENT_PATH |
slipstream-client |
DNSTM_TEST_SSLOCAL_PATH |
sslocal |
Example:
export DNSTM_TEST_DNSTT_CLIENT_PATH=/usr/local/bin/dnstt-client
make test-e2e| Binary | Linux | macOS | Windows |
|---|---|---|---|
| dnstt-* | amd64, arm64 | amd64, arm64 | amd64, arm64 |
| slipstream-* | amd64, arm64 | - | - |
| ss* | amd64, arm64 | amd64, arm64 | - |
| microsocks | manual install | manual install | - |
Check internet connectivity. The test will print which binary failed to download.
To use local binaries instead:
export DNSTM_TEST_DNSTT_CLIENT_PATH=/path/to/dnstt-client
export DNSTM_DNSTT_SERVER_PATH=/path/to/dnstt-servermicrosocks must be installed manually (no auto-download):
# Fedora/RHEL
sudo dnf install microsocks
# Debian/Ubuntu
sudo apt install microsocks
# From source
git clone https://github.com/rofl0r/microsocks
cd microsocks && make && sudo make installlsof -i :5310go test -v -timeout 10m ./tests/e2e/...The script scripts/remote-e2e.sh automates the full remote testing workflow against a server with dnstm deployed. It builds, deploys, installs, creates tunnels, and validates connectivity across all transport/backend combinations.
Local machine:
go(for building the binary)jq,curlslipstream-client,dnstt-client,sslocalin$PATH- SSH access to the target server
Remote server:
- NS records pointing test domains to the server IP
- SSH root access
Copy the example config and fill in your SSH target, domains, and credentials:
cp scripts/e2e-config.json.example scripts/e2e-config.json{
"ssh_target": "user@host-or-ssh-alias",
"dns_resolver": "8.8.8.8",
"domains": {
"dnstt_socks": "dnstt-socks.example.com",
"dnstt_ssh": "dnstt-ssh.example.com",
"slip_socks": "slip-socks.example.com",
"slip_ssh": "slip-ssh.example.com",
"slip_ss": "slip-ss.example.com"
},
"shadowsocks": {
"multi": { "method": "aes-256-gcm", "password": "..." },
"single": { "method": "chacha20-ietf-poly1305", "password": "..." }
}
}ssh_target: SSH config alias oruser@host(required)dns_resolver: public DNS resolver for client connections (default:8.8.8.8)
# Run all phases
./scripts/remote-e2e.sh
# Custom config file
./scripts/remote-e2e.sh -c my-config.json
# Run only a specific phase
./scripts/remote-e2e.sh --phase multi| Phase | What it tests |
|---|---|
single |
Fresh install, tunnel add for all 5 types, each tested individually in single mode |
multi |
Setup multi-mode state via config load, test 3 tunnels simultaneously |
mode-switch |
Switch multi→single→multi, verify tunnels work after each switch |
config-load |
Clean reinstall, config load with multi config, verify tunnels work |
config-reload |
config load single config over existing multi, validate old resources cleaned up |
Each phase is standalone — it sets up its own prerequisite state and can be run independently via --phase.
After all phases complete, the script generates temp/e2e-connections.md with connection commands for each tunnel on the server, along with fetched certs/pubkeys in temp/certs/.
| Tag | Transport | Backend | Test method |
|---|---|---|---|
| slip-socks | Slipstream | SOCKS | slipstream-client → curl |
| slip-ssh | Slipstream | SSH | slipstream-client → ssh -D → curl |
| slip-ss | Slipstream | Shadowsocks | slipstream-client → sslocal → curl |
| dnstt-socks | DNSTT | SOCKS | dnstt-client → curl |
| dnstt-ssh | DNSTT | SSH | dnstt-client → ssh -D → curl |
All tests validate that curl -x socks5h://127.0.0.1:PORT https://httpbin.org/ip returns the server's public IP.
- Each test uses a unique local port (incrementing from 10800) to avoid conflicts
- Certificates and public keys are fetched from the remote and cached per phase
- Client processes are automatically cleaned up after each test and on script exit
- Individual test failures are counted but don't abort the run
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Run unit tests
run: make test-unit
- name: Run integration tests
run: make test-integration
- name: Run E2E tests
run: make test-e2e