dnstunnel

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

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

Go to latest
Published: Sep 9, 2026 License: GPL-3.0 Imports: 28 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.
  • Fast Lane over TCP: Point the client's servers at a tcp:// resolver (or any resolver forwarding over TCP) and the server serves ~8 KiB chunks — no datagram size limit applies. Measured ~2× over plain UDP on loopback.
  • Upstream DNS Transports: Client supports standard UDP/TCP DNS, DNS-over-TLS (tls:// / dot://), and DNS-over-HTTPS (https:// / doh://).
  • Network-Change Recovery: pooled DNS sockets are discarded after transport or DNS-response failures, polling survives resolver/interface outages, and an in-flight write retries with capped backoff until its context or write deadline expires. Authoritative NXDOMAIN is treated as an expired server session instead of a successful write.
  • 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.
Throughput ladder (same server, loopback): plain UDP ≈ 1.6 MB/s, EDNS0 ≈ 2.8 MB/s, tcp:// upstream ≈ 2.9 MB/s.
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.

Network recovery preserves the current session while the server still knows it: temporary resolver, interface, UDP/TCP, DoT, and DoH failures cause queries to move onto fresh transports and retry. Set a write deadline when the application needs a bounded outage window. If the authoritative endpoint returns NXDOMAIN, the server-side session and backend stream are gone; the connection terminates with ErrServerSessionGone because silently attaching the existing byte stream to a new backend would corrupt TCP semantics. The CLI's UDP forwarding mode removes that failed peer generation and creates a new tunnel on its next local datagram.

Per-session backends (DialTarget / DialUDPTarget): one Client can reach different backends on different sessions — e.g. SSH over a tcp:// session and WireGuard over a udp:// session — without constructing a client per backend. Each declaration is validated by the server's allow_targets list:

sshConn, err := cli.DialTarget(ctx, "tcp://127.0.0.1:22")     // stream to sshd
wgConn, err := cli.DialUDPTarget(ctx, "udp://10.8.0.1:51820") // datagrams to WireGuard

Custom TLS for DoT/DoH upstreams (ClientConfig.TLSConfig): pass a *tls.Config to control the TLS layer of tls:///dot:// resolvers and https:// DoH endpoints — self-signed CAs, SNI, InsecureSkipVerify:

cli, err := dnstunnel.NewClient(dnstunnel.ClientConfig{
	Domain:    "tunnel.example.com",
	Servers:   []string{"dot://dns.internal:853"},
	TLSConfig: &tls.Config{RootCAs: internalCAPool}, // or InsecureSkipVerify for tests
})

Typed event callbacks (EventHandler): instead of (or besides) logs, the library drives your application logic with strongly-typed events. Handlers run on a dedicated goroutine with per-event panic recovery and can never block the data path — events are queued (bounded, oldest dropped when overwhelmed):

// Client side: session lifecycle + security signals.
cli, _ := dnstunnel.NewClient(dnstunnel.ClientConfig{
	Domain: "tunnel.example.com",
	Servers: []string{"8.8.8.8:53"},
	EventHandler: func(ev dnstunnel.ClientEvent) {
		switch ev.Kind {
		case dnstunnel.ClientTunnelEstablished: // handshake + declaration done
		case dnstunnel.ClientReconnecting:      // ev.Attempt, ev.Err
		case dnstunnel.ClientTunnelDied:        // ev.Reason ("closed by caller", "write deadline exceeded", ...)
		case dnstunnel.ClientTargetDenied:      // ev.Target refused by the allow list
		}
	},
})
// Or install later: cli.SetEventHandler(handler)
// Context-style death signaling on each session:
//   tunnel.Done() <-chan struct{}  — closed when the session dies
//   tunnel.Err() error             — why (nil for a deliberate Close)

// Server side: session lifecycle + SIEM-ready security kinds.
srv, _ := dnstunnel.NewServer(dnstunnel.ServerConfig{
	// ...
	EventHandler: func(ev dnstunnel.ServerEvent) {
		// SessionCreated / SessionClosed / AuthRejected / ReplayDropped / TargetDenied
	},
})

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.

Wire layout v2

Clients probe the server with a "tunnel2"-marked query once per session; a server that answers gets the v2 layout, in which the session label moves to the front and upstream payloads span multiple labels (~2.4× upstream bytes per query). The probe rides the target-declaration exchange, so it costs no extra round trip. Sessions fall back to the v1 layout transparently when the probe goes unanswered.

Throughput paths

Downstream throughput is bounded by the response budget of the upstream transport: ~200-byte chunks under the legacy 512-byte UDP limit, ~800-byte chunks with EDNS0 ("edns0" on both ends), and ~8 KiB chunks when queries arrive over TCP (tcp:// upstreams or resolvers forwarding over TCP — DNS/TCP messages are length-prefixed and not datagram-bound). Upstream pollers (three per path, additive in-flight windows on both directions) pipeline the round trips; clients advertise their downstream flow-control window in every poll.

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. DialTarget / DialUDPTarget declare the backend per session instead of per client, so one Client can reach several backends (e.g. SSH over tcp:// and WireGuard over udp://) validated by the same allow list. ClientConfig.TLSConfig customizes the TLS layer of tls://, dot:// and https:// upstream resolvers (root CAs, SNI, skip-verify).

Event callbacks

Instead of (or besides) logs, embedders can drive business logic from typed events: ClientConfig.EventHandler / Client.SetEventHandler receive TunnelEstablished, Reconnecting, TunnelDied (with the death reason) and TargetDenied; ServerConfig.EventHandler / Server.SetEventHandler receive SessionCreated, SessionClosed and the SIEM-ready security kinds AuthRejected, ReplayDropped, TargetDenied. Handlers are dispatched on a dedicated goroutine with per-event panic recovery; publishing is bounded and never blocks the data path. Each client session also exposes Done() <-chan struct{} and Err() error, context-style, for death-only signaling.

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

Index

Constants

View Source
const (
	ReasonCallerClosed      = "closed by caller"
	ReasonWriteFailed       = "write failed"
	ReasonWriteTimeout      = "write deadline exceeded"
	ReasonCtxCancelled      = "context cancelled"
	ReasonServerSessionGone = "server session no longer exists"
	// ReasonMaxRetries is retained for source compatibility. Current clients
	// retry transient transport failures until a deadline or cancellation.
	ReasonMaxRetries = "write failed after max retries"
)

Common TunnelDied reasons carried in ClientEvent.Reason.

Variables

View Source
var ErrServerSessionGone = errors.New("dnstunnel: server session no longer exists")

ErrServerSessionGone means an authoritative tunnel endpoint no longer knows the session. Transport failures are deliberately kept distinct: they may recover on the same session after an interface or resolver change.

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; DialTarget / DialUDPTarget declare the backend per session instead of per client.

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. The backend is the client's configured Target (server default when empty); to declare the backend per session, use DialTarget. A declared udp:// target needs DialUDP.

func (*Client) DialTarget

func (c *Client) DialTarget(ctx context.Context, target string) (net.Conn, error)

DialTarget is Dial with the backend declared for this one session ("tcp://" or "udp://host:port"; empty means the server default). The declaration is validated by the server's allow list and answered with the transport that actually applies.

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

The backend is the client's configured Target (server default when empty); to declare the backend per session, use DialUDPTarget. A declared tcp:// target needs Dial.

func (*Client) DialUDPTarget

func (c *Client) DialUDPTarget(ctx context.Context, target string) (net.PacketConn, error)

DialUDPTarget is DialUDP with the backend declared for this one session ("udp://host:port"; empty means the server default). The declaration is validated by the server's allow list and answered with the transport that actually applies.

func (*Client) SetEventHandler

func (c *Client) SetEventHandler(h ClientEventHandler)

SetEventHandler installs or replaces the client event handler (nil disables delivery). Applies to sessions dialed afterwards AND to sessions already running — the dispatcher is shared, so a mid-flight handler swap affects every open tunnel of this client.

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:"-"`
	// TLSConfig optionally customizes the TLS layer of tls:// / dot:// paths
	// (self-signed CAs, SNI, skip-verify) and of https:// DoH endpoints. When
	// nil, system defaults apply and certificate verification is standard.
	TLSConfig *tls.Config `json:"-"`
	// EventHandler receives typed tunnel lifecycle events (established, died,
	// reconnecting, target denied). Handlers run on a dedicated goroutine with
	// per-event panic recovery; they must not block the data path. May be set
	// or swapped any time via Client.SetEventHandler.
	EventHandler ClientEventHandler `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 ClientEvent

type ClientEvent struct {
	Kind      ClientEventKind
	Session   string // tunnel session ID
	Target    string // declared backend, "" = server default
	Transport string // confirmed backend transport ("tcp"/"udp"), "" = unknown
	Reason    string // TunnelDied: why the session died
	Attempt   int    // Reconnecting: the 1-based retry number
	Err       error  // underlying error, when applicable
}

ClientEvent describes one lifecycle occurrence on a client tunnel session.

type ClientEventHandler

type ClientEventHandler func(ClientEvent)

ClientEventHandler receives client tunnel events. Called from the event dispatcher goroutine — never from the data path.

type ClientEventKind

type ClientEventKind int

Client events. Handlers are dispatched on a dedicated goroutine with a panic guard per event: they may drive business logic, but they can never block the tunnel's send/receive loops — publishing an event never waits on a handler and never touches the data path.

const (
	// ClientTunnelEstablished fires once per session, after the Noise
	// handshake, target declaration and pollers are all in place.
	ClientTunnelEstablished ClientEventKind = iota
	// ClientTunnelDied fires when the session dies (or is closed). The Reason
	// field explains which; Err carries the underlying error, if any.
	ClientTunnelDied
	// ClientReconnecting fires on every retry of an upstream chunk after its
	// first attempt failed. Attempt is the 1-based retry number. The session
	// usually survives; a permanent failure is followed by TunnelDied.
	ClientReconnecting
	// ClientTargetDenied fires when the server's allow list refused the
	// declared target; the Dial call returns the same condition as an error.
	ClientTargetDenied
)

func (ClientEventKind) String

func (k ClientEventKind) String() string

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

Close tears the session down. Done() closes and Err() reports nil — the caller ended it deliberately.

func (*DNSClientTunnel) Done

func (t *DNSClientTunnel) Done() <-chan struct{}

Done returns a channel that closes when the session dies for any reason (context cancelled, write failed, explicit Close). context-style: combine with Err() to learn why.

func (*DNSClientTunnel) Err

func (t *DNSClientTunnel) Err() error

Err returns the reason the session died, or nil while it is alive (and nil after a deliberate caller Close).

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

func (*Server) SetEventHandler

func (s *Server) SetEventHandler(h ServerEventHandler)

SetEventHandler installs or replaces the server event handler (nil disables delivery). Call before Run.

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:"-"`
	// EventHandler receives typed session events (created, closed, auth
	// rejected, replay dropped, target denied) — the security kinds are
	// valuable for SIEM pipelines. Handlers run on a dedicated goroutine with
	// per-event panic recovery and never block the DNS query path.
	EventHandler ServerEventHandler `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.

type ServerEvent

type ServerEvent struct {
	Kind      ServerEventKind
	SessionID string
	Remote    string // resolver address the query arrived from, when known
	Target    string // declared backend, for TargetDenied
	Detail    string // human-readable detail, kind-specific
}

ServerEvent describes one lifecycle or security occurrence on the server.

type ServerEventHandler

type ServerEventHandler func(ServerEvent)

ServerEventHandler receives server session events. Called from the event dispatcher goroutine — never from the DNS query path.

type ServerEventKind

type ServerEventKind int

Server events. Valuable for SIEM pipelines: the security kinds (AuthRejected, ReplayDropped, TargetDenied) surface attack and misuse signals that plain logs make easy to miss.

const (
	// ServerSessionCreated fires when a tunnel session is registered.
	ServerSessionCreated ServerEventKind = iota
	// ServerSessionClosed fires when a tunnel session is torn down for any
	// reason (client close signal, idle expiry, backend failure).
	ServerSessionClosed
	// ServerAuthRejected fires when a client fails the Noise handshake (bad
	// or missing ephemeral key).
	ServerAuthRejected
	// ServerReplayDropped fires when a duplicate or replayed upstream chunk is
	// dropped by the deduplication window.
	ServerReplayDropped
	// ServerTargetDenied fires when a declared target is refused by the
	// allow_targets list.
	ServerTargetDenied
)

func (ServerEventKind) String

func (k ServerEventKind) String() string

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