dnstunnel

package module
v0.0.0-...-c078274 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: GPL-3.0 Imports: 27 Imported by: 0

README

dns_custom

High-Performance Authoritative DNS Tunnel Server & Client with Noise_NK Curve25519 AEAD Encryption.

Features

  • Noise Protocol Encryption: Optional Noise_NK_25519_ChaChaPoly_BLAKE2s cryptographic channel.
  • Dual Target Forwarding: Supports both TCP (tcp://host:port) and UDP (udp://host:port) backend services.
  • UDP Datagram Mode: Client can tunnel UDP datagrams (DialUDP / "target": "udp://...") with boundaries preserved via length framing — e.g. for WireGuard, QUIC or game servers. The local transport follows the target automatically; the server refuses transport mismatches.
  • Client-Declared Targets with ACL: Clients can declare the backend they want per session; the server validates it against an allow_targets pattern list ("tcp://127.0.0.1:*", "udp://10.8.0.*:51820") and always answers with the transport that applies. One server can safely serve many different backends.
  • Embeddable Go Library: The root package dnstunnel exposes Server.Run(ctx) / Client.Dial(ctx) (net.Conn) / Client.DialUDP(ctx) (net.PacketConn), so external programs can borrow the tunnel through standard connection interfaces (see Using as a Go Library).
  • 8 DNS Record Types: Supports TXT, NULL, CNAME, A, AAAA, MX, SRV, and NS.
  • Optional EDNS0: With "edns0": true on both ends, answers announce a 1232-byte UDP budget instead of 512 — much larger downstream chunks and near-doubled throughput.
  • Upstream DNS Transports: Client supports standard UDP/TCP DNS, DNS-over-TLS (tls:// / dot://), and DNS-over-HTTPS (https:// / doh://).
  • Unified JSON & Env Configuration: Configuration is loaded only via -c <config.json> — config files are never auto-discovered from the working directory, so the process always runs with the config you named. DNSCUSTOM_* environment variables may override fields present in the loaded file (handy for Docker), but cannot replace the file itself.
  • Stun Node Sharing (gen-uri): One-click sharing URI and terminal ASCII QR code generation for Android & TV.

One-Key Management (Linux Server & Client)

1. Server Installation (Default)
curl -fsSL https://raw.githubusercontent.com/NNdroid/dns_custom/master/scripts/install.sh | sudo bash -s install server
2. Client Installation (Linux)
curl -fsSL https://raw.githubusercontent.com/NNdroid/dns_custom/master/scripts/install.sh | sudo bash -s install client
3. Pin a Release Version (Optional)

Leave APP_VERSION unset to install the latest release. To install a specific raw-binary release, supply its tag (v1.0.yyyyMMdd-<7-character-git-hash>):

curl -fsSL https://raw.githubusercontent.com/NNdroid/dns_custom/master/scripts/install.sh | sudo env APP_VERSION=v1.0.20260904-1a2b3c4 bash -s install server
4. Upgrade / Uninstall
# One-key Upgrade (Keeps existing config.json)
curl -fsSL https://raw.githubusercontent.com/NNdroid/dns_custom/master/scripts/install.sh | sudo bash -s upgrade

# One-key Uninstall
curl -fsSL https://raw.githubusercontent.com/NNdroid/dns_custom/master/scripts/install.sh | sudo bash -s uninstall
5. Service Management
systemctl start dns_custom    # Start service
systemctl stop dns_custom     # Stop service
systemctl restart dns_custom  # Restart service
systemctl status dns_custom   # Check status
journalctl -u dns_custom -f   # View live logs

Configuration Reference (config.json)

Field Type Default Description
mode string "server" Operational mode: "server" (authoritative DNS server) or "client" (local listener).
listen string server ":53", client "127.0.0.1:1080" Server: authoritative DNS listen address. Client: local address to bind — its transport (TCP or UDP) follows the backend automatically (declared target or server default).
target string "tcp://127.0.0.1:22" Server: default backend (tcp://host:port or udp://host:port). Client: optional backend declaration — must pass the server's allow_targets list, or the session is rejected.
allow_targets array [] Server only: patterns granting client-declared targets, e.g. "tcp://127.0.0.1:*", "udp://10.8.0.*:51820"; scheme/host/port may each be *, host wildcards never cross a dot. Empty = default target only; "*" allows everything (dangerous).
max_sessions int 0 Server only: concurrent session cap (each session holds up to ~600KB of buffers). 0 = unlimited.
edns0 bool false Announce 1232-byte UDP answers via EDNS0 instead of the 512-byte limit — much larger downstream chunks. Set on both ends or the tunnel stalls.
domain string "" Authoritative DNS tunnel domain (e.g. tunnel.example.com).
privkey string "" Server static private key for Noise encryption (Hex or Base64). Generate with dns_custom gen-keys.
pubkey string "" Server static public key for Noise encryption in client mode (Hex or Base64).
servers `array string` ["8.8.8.8:53", "1.1.1.1:53"]
record_type string "txt" Tunnel query DNS record type: txt, null, cname, a, aaaa, mx, srv, ns.
log_level string "info" Logging output level: debug, info, warn, error.

Deployment Examples

SSH over the Tunnel (TCP)

Server forwards to a local sshd; the client exposes a local TCP port:

// server config.json
{
  "mode": "server",
  "listen": ":53",
  "target": "tcp://127.0.0.1:22",
  "domain": "t.example.com",
  "privkey": "<server private key>"
}
// client config.json
{
  "mode": "client",
  "listen": "127.0.0.1:1080",
  "domain": "t.example.com",
  "pubkey": "<server public key>",
  "servers": ["8.8.8.8:53", "1.1.1.1:53"],
  "record_type": "txt"
}
WireGuard over the Tunnel (UDP Datagrams)

The client's local transport follows its target automatically: declaring a udp:// target binds a local UDP socket with boundary-preserving datagram forwarding. Point the local WireGuard peer's endpoint at the client's listen address:

// server
{ "mode": "server", "target": "udp://127.0.0.1:51820", "...": "..." }

// client
{ "mode": "client", "listen": "127.0.0.1:51820", "target": "udp://127.0.0.1:51820", "...": "..." }

Omit the client target to use the server default — the client probes the server at startup and binds TCP or UDP accordingly.

Gateway Mode (Client-Declared Targets + ACL)

One server can serve several backends. Clients declare the backend they want; the server honors only declarations that pass allow_targets and rejects the rest with an explicit error:

// server
{
  "mode": "server",
  "target": "tcp://127.0.0.1:22",
  "allow_targets": ["tcp://127.0.0.1:*", "udp://127.0.0.1:*"],
  "max_sessions": 256
}
client A: "target": "tcp://127.0.0.1:22"    → local TCP listener
client B: "target": "udp://127.0.0.1:51820" → local UDP listener

An empty allow_targets (the default) refuses every declaration — only the server default target is reachable. Patterns match literally against the declared address; prefer IPs, since hostnames are never resolved during matching.

Environment Variables

DNSCUSTOM_* variables override fields present in the config file loaded via -c (handy for Docker); short aliases exist for a few of them (MODE, LISTEN, PORT, TYPE, LOGLEVEL):

Variable Overrides
DNSCUSTOM_MODE mode
DNSCUSTOM_LISTEN listen
DNSCUSTOM_TARGET target
DNSCUSTOM_DOMAIN domain
DNSCUSTOM_PRIVKEY privkey
DNSCUSTOM_PUBKEY pubkey
DNSCUSTOM_SERVERS servers (comma-separated)
DNSCUSTOM_RECORD_TYPE record_type
DNSCUSTOM_ALLOW_TARGETS allow_targets (comma-separated)
DNSCUSTOM_MAX_SESSIONS max_sessions
DNSCUSTOM_EDNS0 edns0 (1/true/yes/on)
DNSCUSTOM_LOG_LEVEL log_level

Quick Start

1. Generate Noise Keypair (Optional)
dns_custom gen-keys
dns_custom gen-uri -c /etc/dns_custom/config.json

Using as a Go Library

The root package dnstunnel is a library; the CLI in cmd/dns_custom is just one consumer. External programs can borrow the tunnel for unified, encrypted access to backend services:

import dnstunnel "github.com/NNdroid/dns_custom"

// Client: every Dial opens an independent tunnel session.
cli, err := dnstunnel.NewClient(dnstunnel.ClientConfig{
	Domain:     "tunnel.example.com",
	Servers:    []string{"8.8.8.8:53", "1.1.1.1:53"},
	RecordType: "txt",
	PublicKey:  serverPubKey, // optional Noise_NK key
})

conn, err := cli.Dial(ctx)     // stream access → net.Conn
pconn, err := cli.DialUDP(ctx) // datagram access → net.PacketConn

// Optionally declare which backend to reach; the server validates it against
// its allow_targets list. Without a declaration, ask the server what its
// default target transport is (e.g. to pick a local UDP or TCP bind):
cli2, _ := dnstunnel.NewClient(dnstunnel.ClientConfig{
	Domain:  "tunnel.example.com",
	Servers: []string{"8.8.8.8:53"},
	Target:  "udp://10.8.0.1:51820",
})
transport, _ := cli2.DefaultTarget(ctx) // "tcp" or "udp"

// Server: terminates sessions and forwards to the backend.
srv, err := dnstunnel.NewServer(dnstunnel.ServerConfig{
	ListenAddr:   ":53",
	TargetAddr:   "tcp://127.0.0.1:22", // or "udp://127.0.0.1:51820"
	Domain:       "tunnel.example.com",
	PrivateKey:   serverPrivKey,
	AllowTargets: []string{"tcp://127.0.0.1:*", "udp://127.0.0.1:*"},
})
err = srv.Run(ctx) // blocks; returns nil on clean ctx cancellation

Dial returns a net.Conn and DialUDP a net.PacketConn, so the tunnel plugs directly into http.Transport.DialContext, database drivers, SSH clients and anything else that consumes standard connection interfaces. Logging is injected via the config's Logger field (*zap.SugaredLogger; nil means a nop logger), so the library never touches global logger state or calls os.Exit.

Build the CLI from source with:

go build -o dns_custom ./cmd/dns_custom

Development

go test ./...        # full test suite
go test -race ./...  # with race detector (needs CGO and a C toolchain)

CI (.github/workflows/test.yml) runs vet plus plain and race-enabled tests on Linux, macOS and Windows for every push and pull request. Pushing a v1.0.yyyyMMdd-<short-sha> tag triggers .github/workflows/release.yml, which re-runs the tests and publishes raw binaries for 9 platforms.

Documentation

Overview

Client side of the DNS tunnel: dials out through upstream DNS resolvers.

Package dnstunnel implements a high-performance DNS tunnel with optional Noise_NK Curve25519 AEAD encryption.

Embedding the tunnel in other programs

The tunnel can be used as a library so external programs can borrow it for unified, encrypted access to backend services:

// Client side: every Dial opens an independent tunnel session.
cli, err := dnstunnel.NewClient(dnstunnel.ClientConfig{
	Domain:     "tunnel.example.com",
	Servers:    []string{"8.8.8.8:53", "1.1.1.1:53"},
	RecordType: "txt",
	PublicKey:  serverPubKey, // optional Noise_NK key
})
conn, err := cli.Dial(ctx)          // stream access (net.Conn)
pconn, err := cli.DialUDP(ctx)      // datagram access (net.PacketConn)

// Server side: terminates sessions and forwards to the backend.
srv, err := dnstunnel.NewServer(dnstunnel.ServerConfig{
	ListenAddr: ":53",
	TargetAddr: "tcp://127.0.0.1:22", // or "udp://127.0.0.1:51820"
	Domain:     "tunnel.example.com",
	PrivateKey: serverPrivKey,
})
err = srv.Run(ctx) // blocks; returns nil on clean ctx cancellation

Dial returns a net.Conn and DialUDP a net.PacketConn, so the tunnel plugs directly into http.Transport.DialContext, database drivers, SSH clients and anything else that consumes standard connection interfaces.

The server routes sessions by the marker inside the session ID: plain stream sessions follow the configured target scheme (tcp:// backends receive a byte stream), while sessions whose ID carries the UDP marker (created by DialUDP) are forwarded as length-framed datagrams over UDP. The session transport must match the target scheme — a UDP-marker session against a tcp:// target is refused, because datagram semantics cannot be preserved toward a stream backend. Stream sessions against udp:// targets are the legacy pre-datagram behavior (datagram boundaries are not preserved) and are kept only for compatibility with older clients.

Client-declared targets

A client may declare the backend it wants per configuration (ClientConfig Target) or per session. The server validates the declaration against ServerConfig AllowTargets — a list of patterns such as "tcp://127.0.0.1:*" or "udp://10.8.0.*:51820" where each of scheme, host and port may be "*" and host wildcards never cross a dot. An empty AllowTargets list means clients cannot override the target. Every exchange answers with the transport that actually applies ("tcp" or "udp"), declared or default, so callers always know which kind of local socket to bind; Client.DefaultTarget probes it without declaring anything.

Server-side of the DNS tunnel: terminates tunnel sessions and forwards their byte streams (or framed UDP datagrams) to a configured backend.

Index

Constants

This section is empty.

Variables

View Source
var Version = "1.4.0"

Version is the release version of the tool. Release builds override it via -ldflags "-X github.com/NNdroid/dns_custom.Version=<version>".

Functions

func FormatNoiseKey

func FormatNoiseKey(key [32]byte) (hexStr, b64Str string)

FormatNoiseKey formats a 32-byte key to hex and base64

func ParseNoiseKey

func ParseNoiseKey(s string) ([32]byte, error)

ParseNoiseKey parses a 32-byte key from hex or base64 string

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the library entry point for dialing out through the DNS tunnel. One Client can open any number of independent tunnel sessions via Dial / DialUDP.

func NewClient

func NewClient(cfg ClientConfig) (*Client, error)

NewClient validates the configuration and returns a Client. The public key, when set, is parsed once here so a typo fails at startup instead of on the first dial.

func (*Client) DefaultTarget

func (c *Client) DefaultTarget(ctx context.Context) (string, error)

DefaultTarget asks the server which transport its default target uses ("tcp" or "udp"). It opens a throwaway session, declares no target, and reads the server's answer — this is how a caller that has no declared target learns which local socket to bind. A server that predates target declarations (or refuses the empty declaration) yields an error.

func (*Client) Dial

func (c *Client) Dial(ctx context.Context) (net.Conn, error)

Dial opens a new tunnel session and returns it as a stream net.Conn. Each call establishes an independent session (Noise handshake, target declaration, pollers, adaptive window) terminated on the server's backend. When the client has a declared target it must be tcp:// — datagram targets need DialUDP.

func (*Client) DialUDP

func (c *Client) DialUDP(ctx context.Context) (net.PacketConn, error)

DialUDP opens a new tunnel session that carries UDP datagrams to the server's UDP backend. Datagrams are length-framed over the tunnel byte stream, so datagram boundaries survive the trip (unlike the legacy stream mode, which reassembles upstream chunks without preserving boundaries).

When the client has a declared target it must be udp://; the declaration is validated by the server's allow list. Without a declaration the session uses the server's default target, which must itself be udp://.

type ClientConfig

type ClientConfig struct {
	Domain     string             `json:"domain"`
	Servers    []string           `json:"servers"`
	RecordType string             `json:"record_type"`
	PublicKey  string             `json:"pubkey"`
	Target     string             `json:"target,omitempty"`
	EDNS0      bool               `json:"edns0,omitempty"`
	Logger     *zap.SugaredLogger `json:"-"`
	// Dialer optionally controls sockets used by UDP, TCP, DoT and DoH paths.
	Dialer *net.Dialer `json:"-"`
}

ClientConfig configures a Client. Logger may be left nil for a silent client; the CLI injects its own zap logger here.

Target optionally declares the backend the client wants sessions forwarded to ("tcp://host:port" or "udp://host:port"; host:port alone means tcp). The server only honors it when the target passes its allow_targets list, and the server's answer tells the caller which transport actually applies (see DefaultTarget and DNSClientTunnel.Transport). Leave empty to use whatever default target the server is configured with.

type DNSClientTunnel

type DNSClientTunnel struct {
	// contains filtered or unexported fields
}

DNSClientTunnel is one tunnel session: a reliable ordered byte stream over DNS queries, optionally encrypted with Noise_NK. It implements net.Conn, so it can be handed directly to io.Copy, http.Transport.DialContext, database drivers and anything else that consumes connections.

func NewDNSClientTunnel

func NewDNSClientTunnel(ctx context.Context, servers []string, domain string, recordType string, pubKeyStr string) (*DNSClientTunnel, error)

NewDNSClientTunnel opens a single stream tunnel session. Library users usually want Client.Dial instead, which is this constructor behind a reusable, pre-validated Client.

func (*DNSClientTunnel) Close

func (t *DNSClientTunnel) Close() error

func (*DNSClientTunnel) LocalAddr

func (t *DNSClientTunnel) LocalAddr() net.Addr

LocalAddr and RemoteAddr are pseudo addresses identifying this tunnel session; the tunnel has no real socket-level endpoints.

func (*DNSClientTunnel) Read

func (t *DNSClientTunnel) Read(p []byte) (int, error)

func (*DNSClientTunnel) RemoteAddr

func (t *DNSClientTunnel) RemoteAddr() net.Addr

func (*DNSClientTunnel) SetDeadline

func (t *DNSClientTunnel) SetDeadline(deadline time.Time) error

SetDeadline sets both the read and the write deadline. A zero time disables the deadline. An expired deadline unblocks pending Read/Write calls with os.ErrDeadlineExceeded.

func (*DNSClientTunnel) SetReadDeadline

func (t *DNSClientTunnel) SetReadDeadline(deadline time.Time) error

func (*DNSClientTunnel) SetWriteDeadline

func (t *DNSClientTunnel) SetWriteDeadline(deadline time.Time) error

func (*DNSClientTunnel) Transport

func (t *DNSClientTunnel) Transport() string

Transport reports the backend transport the server confirmed for this session ("tcp" or "udp"). It is set once the target declaration exchange completes; sessions without a declared target learn nothing here and follow the server's default.

func (*DNSClientTunnel) Write

func (t *DNSClientTunnel) Write(p []byte) (int, error)

type DNSServer

type DNSServer struct {
	// contains filtered or unexported fields
}

func NewDNSServer

func NewDNSServer(cfg ServerConfig) (*DNSServer, error)

NewDNSServer builds the tunnel DNS handler. Use this when embedding the handler in an externally managed dns.Server; most callers want NewServer instead.

func (*DNSServer) ServeDNS

func (s *DNSServer) ServeDNS(w dns.ResponseWriter, req *dns.Msg)

type NoiseCipherState

type NoiseCipherState struct {
	// contains filtered or unexported fields
}

NoiseCipherState wraps the AEAD derived from a Noise_NK handshake.

The nonce is NOT a stream counter: it is supplied explicitly by the caller and is derived from the transport sequence number (upstream = dataSeq, downstream = serverSeq). An auto-incrementing nonce implicitly assumes one encryption per successful delivery in both directions, which DNS cannot provide - queries and answers are lost, duplicated, reordered and retransmitted. With a sequence-derived nonce every message is self-describing: it can arrive any number of times, in any order, and still decrypt, which is exactly what the reliability layer needs.

func (*NoiseCipherState) Decrypt

func (s *NoiseCipherState) Decrypt(seq uint64, ciphertext []byte) ([]byte, error)

Decrypt opens ciphertext using the nonce derived from seq.

func (*NoiseCipherState) Encrypt

func (s *NoiseCipherState) Encrypt(seq uint64, plaintext []byte) []byte

Encrypt seals plaintext under the nonce derived from seq. Same seq + same plaintext always yields the same ciphertext, so retransmissions are byte-identical.

type NoiseKeyPair

type NoiseKeyPair struct {
	PrivateKey [32]byte
	PublicKey  [32]byte
}

NoiseKeyPair represents a Curve25519 public/private keypair

func GenerateNoiseKeyPair

func GenerateNoiseKeyPair() (*NoiseKeyPair, error)

GenerateNoiseKeyPair generates a random Curve25519 keypair

type NoiseSession

type NoiseSession struct {
	SendCipher *NoiseCipherState
	RecvCipher *NoiseCipherState
}

NoiseSession manages bidirectional encrypted channel derived from Noise_NK handshake

func NewClientNoiseSession

func NewClientNoiseSession(serverPubkey [32]byte) (*NoiseSession, []byte, error)

NewClientNoiseSession initiates Noise_NK handshake against server public key Returns (NoiseSession, clientEphemeralPubkeyBytes, error)

func NewServerNoiseSession

func NewServerNoiseSession(serverPrivkey [32]byte, clientEPub []byte) (*NoiseSession, error)

NewServerNoiseSession derives keys on server side using server static private key and client ephemeral public key

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the library entry point for terminating DNS tunnel sessions and forwarding them to a backend. Run binds the authoritative DNS listener and blocks until the context is cancelled or the listener fails.

func NewServer

func NewServer(cfg ServerConfig) (*Server, error)

NewServer validates the configuration, loads the Noise private key (if any) and returns a ready-to-run Server.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run serves tunnel queries on UDP and TCP until ctx is cancelled (returns nil) or a listener fails (returns that error).

type ServerConfig

type ServerConfig struct {
	ListenAddr   string             `json:"listen"`
	TargetAddr   string             `json:"target"`
	Domain       string             `json:"domain"`
	PrivateKey   string             `json:"privkey"`
	AllowTargets []string           `json:"allow_targets,omitempty"`
	MaxSessions  int                `json:"max_sessions,omitempty"` // concurrent session cap; 0 = unlimited
	EDNS0        bool               `json:"edns0,omitempty"`        // announce 1232-byte UDP answers via EDNS0 (both ends must agree)
	Logger       *zap.SugaredLogger `json:"-"`
}

ServerConfig configures a Server. Logger may be left nil for a silent server; the CLI injects its own zap logger here.

AllowTargets gates client-declared targets (see flagTarget). It is a list of patterns like "tcp://127.0.0.1:*" or "udp://10.8.0.*:51820"; scheme, host and port may each be "*". An empty list means clients cannot override the target: every session uses TargetAddr. The special pattern "*" allows any target.

Directories

Path Synopsis
cmd
dns_custom command
Command dns_custom is the CLI for the dnstunnel library: it loads the JSON configuration, injects logging and runs the tunnel as a standalone server or client process.
Command dns_custom is the CLI for the dnstunnel library: it loads the JSON configuration, injects logging and runs the tunnel as a standalone server or client process.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL