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
- Variables
- func FormatNoiseKey(key [32]byte) (hexStr, b64Str string)
- func ParseNoiseKey(s string) ([32]byte, error)
- type Client
- func (c *Client) DefaultTarget(ctx context.Context) (string, error)
- func (c *Client) Dial(ctx context.Context) (net.Conn, error)
- func (c *Client) DialTarget(ctx context.Context, target string) (net.Conn, error)
- func (c *Client) DialUDP(ctx context.Context) (net.PacketConn, error)
- func (c *Client) DialUDPTarget(ctx context.Context, target string) (net.PacketConn, error)
- func (c *Client) SetEventHandler(h ClientEventHandler)
- type ClientConfig
- type ClientEvent
- type ClientEventHandler
- type ClientEventKind
- type DNSClientTunnel
- func (t *DNSClientTunnel) Close() error
- func (t *DNSClientTunnel) Done() <-chan struct{}
- func (t *DNSClientTunnel) Err() error
- func (t *DNSClientTunnel) LocalAddr() net.Addr
- func (t *DNSClientTunnel) Read(p []byte) (int, error)
- func (t *DNSClientTunnel) RemoteAddr() net.Addr
- func (t *DNSClientTunnel) SetDeadline(deadline time.Time) error
- func (t *DNSClientTunnel) SetReadDeadline(deadline time.Time) error
- func (t *DNSClientTunnel) SetWriteDeadline(deadline time.Time) error
- func (t *DNSClientTunnel) Transport() string
- func (t *DNSClientTunnel) Write(p []byte) (int, error)
- type DNSServer
- type NoiseCipherState
- type NoiseKeyPair
- type NoiseSession
- type Server
- type ServerConfig
- type ServerEvent
- type ServerEventHandler
- type ServerEventKind
Constants ¶
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 ¶
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.
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 ¶
FormatNoiseKey formats a 32-byte key to hex and base64
func ParseNoiseKey ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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.
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.
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.
type NoiseKeyPair ¶
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 ¶
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. |