Testing Guide

April 2, 2026 · View on GitHub

This document describes the testing strategy for dnstm.

Overview

dnstm uses a three-tier testing approach:

LevelPurposeDependenciesSpeed
UnitTest individual functionsNoneFast (~1s)
IntegrationTest CLI commands with mocked systemdNoneFast (~1s)
E2ETest actual tunnel connectivityAuto-downloadedSlow (~15s+)

Test File Naming

  • *_test.go - Test files live alongside the code they test
  • internal/testutil/ - Shared test utilities
  • tests/integration/ - Integration tests for CLI commands
  • tests/e2e/ - End-to-end tests with real binaries

Running Tests

CommandWhat it runsRequirements
make testUnit + IntegrationNone
make test-unitUnit tests onlyNone
make test-integrationIntegration tests onlyNone
make test-e2eE2E testsInternet (first run)
make test-allUnit + Integration + E2EInternet (first run)
make test-coverageUnit + Integration with coverageNone
make test-realAll tests with real systemdRoot

Unit Tests

make test-unit

# Or specific packages
go test -v ./internal/config/...
go test -v ./internal/keys/...

Integration Tests

make test-integration

Uses MockSystemdManager - no root privileges or external binaries required.

E2E Tests

make test-e2e

E2E tests automatically download required binaries on first run to tests/.testbin/.

Binaries downloaded:

Binary Management

Automatic Download

On first E2E test run, binaries are downloaded to tests/.testbin/. Subsequent runs use cached binaries.

Custom Binary Paths

Use environment variables to override binary paths:

Server binaries (used in production and tests):

VariableBinary
DNSTM_DNSTT_SERVER_PATHdnstt-server
DNSTM_SLIPSTREAM_SERVER_PATHslipstream-server
DNSTM_VAYDNS_SERVER_PATHvaydns-server
DNSTM_SSSERVER_PATHssserver
DNSTM_MICROSOCKS_PATHmicrosocks

Client binaries (test only):

VariableBinary
DNSTM_TEST_DNSTT_CLIENT_PATHdnstt-client
DNSTM_TEST_SLIPSTREAM_CLIENT_PATHslipstream-client
DNSTM_TEST_VAYDNS_CLIENT_PATHvaydns-client
DNSTM_TEST_SSLOCAL_PATHsslocal

Example:

export DNSTM_TEST_DNSTT_CLIENT_PATH=/usr/local/bin/dnstt-client
make test-e2e

Supported Platforms

BinaryLinuxmacOSWindows
dnstt-*amd64, arm64amd64, arm64amd64, arm64
slipstream-*amd64, arm64--
vaydns-*amd64, arm64amd64, arm64amd64
ss*amd64, arm64amd64, arm64-
microsocksmanual installmanual install-

Troubleshooting

Binary Download Fails

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-server

microsocks Not Available

microsocks 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 install

Port Already in Use

lsof -i :5310

E2E Tests Timeout

go test -v -timeout 10m ./tests/e2e/...

Remote E2E Tests

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.

Prerequisites

Local machine:

  • go (for building the binary)
  • jq, curl
  • slipstream-client, dnstt-client, sslocal in $PATH
  • SSH access to the target server

Remote server:

  • NS records pointing test domains to the server IP
  • SSH root access

Setup

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",
    "vaydns_socks": "vaydns-socks.example.com",
    "vaydns_ssh": "vaydns-ssh.example.com"
  },
  "shadowsocks": {
    "multi": { "method": "aes-256-gcm", "password": "..." },
    "single": { "method": "chacha20-ietf-poly1305", "password": "..." }
  },
  "socks_auth": {
    "user": "your-socks-user",
    "password": "your-socks-password"
  }
}
  • ssh_target: SSH config alias or user@host (required)
  • dns_resolver: public DNS resolver for client connections (default: 8.8.8.8)
  • socks_auth: SOCKS5 authentication credentials for auth tests (optional, enables auth tests in single and config-reload phases)

Usage

# 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

Phases

PhaseWhat it tests
singleFresh install, tunnel add for all tunnel types, each tested individually in single mode, SOCKS auth enable/disable with multiple transports
multiSetup multi-mode state via config load, test 3 tunnels simultaneously
mode-switchSwitch multi→single→multi, verify tunnels work after each switch
config-loadClean reinstall, config load with multi config, verify tunnels work
config-reloadconfig load single config (with SOCKS auth) over existing multi, validate cleanup and auth enforcement

Each phase is standalone — it sets up its own prerequisite state and can be run independently via --phase.

Output

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

Tunnel types tested

TagTransportBackendTest method
slip-socksSlipstreamSOCKSslipstream-client → curl
slip-sshSlipstreamSSHslipstream-clientssh -D → curl
slip-ssSlipstreamShadowsocksslipstream-clientsslocal → curl
dnstt-socksDNSTTSOCKSdnstt-client → curl
dnstt-sshDNSTTSSHdnstt-clientssh -D → curl
vaydns-socksVayDNSSOCKSvaydns-client → curl
vaydns-sshVayDNSSSHvaydns-clientssh -D → curl

All tests validate that curl -x socks5h://127.0.0.1:PORT https://httpbin.org/ip returns the server's public IP.

Remote E2E Notes

  • 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

CI Integration

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