This page documents every concrete type that implements the dnsx.Transport interface — the leaf nodes that actually send DNS queries to external resolvers. Coverage includes constructor functions, proxy integration, ECH support, and connection lifecycle.
For how the resolver chooses among these transports at query time, see 2.2 For how responses are cached before or after transport dispatch, see 2.3 For the blocklist filtering layer applied to both queries and responses, see 2.1 For DNSCrypt-specific internals (encryption, certificate validation), see intra/dnscrypt/crypto.go1-20
dnsx.Transport InterfaceEvery transport in this document satisfies the same interface:
| Method | Description |
|---|---|
ID() | Unique transport identifier string |
Type() | Protocol type constant (DOH, DNS53, DOT, etc.) |
Query(pid string, q *dns.Msg, smm *DNSSummary) (*dns.Msg, error) | Send a query and populate the summary |
GetAddr() | Human-readable server address string |
GetRelay() | Returns the relay proxy, if any |
IPPorts() | Resolved IP:port pairs for the server |
P50() | P50 latency estimate in milliseconds |
Status() | Current transport status code |
Stop() | Shut down the transport |
All Query implementations fill in a *x.DNSSummary with fields like Latency, RData, RCode, RTtl, Server, PID, RPID, Status, and Msg.
Sources: intra/dnsx/dnsx.go40-52 intra/dnsx/dnsx_listener.go12-58
Transport types to implementation structs and packages:
Sources: intra/doh/doh.go112-131 intra/dns53/dot.go60 intra/dns53/upstream.go64 intra/dns53/mdns.go57 intra/dns53/goos.go37 intra/dns53/grounded.go27-30 intra/dnscrypt/servers.go73 intra/dnsx/plus.go51-54
Package: intra/doh
Struct: transport
Type constant: dnsx.DOH
rawurl must be https:// (using http:// scheme is accepted and silently upgraded to https://, with skipTLSVerify set to true). Bootstrap IP addresses in addrs are pre-registered via dnsx.RegisterAddrs(id, hostname, addrs). The px parameter may be nil (e.g., for id == dnsx.Default).
Sources: intra/doh/doh.go119-121 intra/doh/doh.go173-177 intra/doh/doh.go192
DoH query execution path:
Sources: intra/doh/doh.go441-470 intra/doh/doh.go498-563 intra/doh/doh.go587-697 intra/doh/doh.go699-712 intra/doh/padding.go33-36
Each proxy gets its own *proxytransport entry in the pxclients map (keyed by proxy ID). The proxytransport struct holds:
p ipn.Proxy — the proxy instancec *http.Client — standard TLS HTTP/2 clientc3 *http.Client — ECH-enabled HTTP/2 client (may be nil)A separate c3 HTTP client is lazily created when ECH config becomes available via getOrCreateEchConfigIfNeeded(). Stale entries are purged by purgeProxyClients() when time.Since(lastpurge) > purgethreshold (1 minute).
Sources: intra/doh/doh.go250-254 intra/doh/doh.go297-329 intra/doh/doh.go363-401 intra/doh/doh.go66
Package: intra/doh
Struct: transport (same as DoH, with embedded *odohtransport)
Type constant: dnsx.ODOH
endpoint is the ODoH proxy URL (optional; may be empty for direct target queries). target is the ODoH resolver URL. The ODoH config is fetched from odohconfigdns (https://1.1.1.1/dns-query).
Sources: intra/doh/doh.go129-131 intra/doh/doh.go193-221 intra/doh/odoh.go30
The odoh.ObliviousDoHConfig (HPKE public key) is fetched and cached. Two methods are tried in order:
TypeHTTPS for the target name, extracts SVCB key 32769 via refreshTargetKeyDNS.GET https://<target>/.well-known/odohconfigs via refreshTargetKeyWellKnown.Queries are built using buildTargetQuery, which encrypts the message using HPKE. Responses are decrypted using OpenAnswer.
Sources: intra/doh/odoh.go48-106 intra/doh/odoh.go139-160 intra/doh/odoh.go163-182 intra/doh/odoh.go202-238
Package: intra/dns53
Struct: dot
Type constant: dnsx.DOT
NewTLSTransport parses the rawurl which may be tls://host[:port], tls:host[:port], or just host[:port]. If the scheme is not tls, it disables TLS verification (skipTLSVerify = true).
Sources: intra/dns53/dot.go65-127 intra/dns53/upstream.go31-33
Sources: intra/dns53/dot.go159-175 intra/dns53/dot.go177-220 intra/dns53/dot.go282-320
DoT uses core.MultConnPool[uint64] keyed by p.Handle() (the proxy's unique handle). The pool is enabled by default (usepool = true).
Sources: intra/dns53/dot.go31 intra/dns53/dot.go52-53 intra/dns53/dot.go248-274
Package: intra/dns53
Struct: transport
Type constant: dnsx.DNS53
| Function | Signature | Use case |
|---|---|---|
NewTransport | (ctx, id, ip, port string, px) | Explicit IP and port |
NewTransportFrom | (ctx, id string, ipp netip.AddrPort, px) | From a netip.AddrPort |
NewTransportFromHostname | (ctx, id, hostOrHostport, ipcsv string, px) | Hostname with pre-resolved IPs |
Sources: intra/dns53/upstream.go69-76 intra/dns53/upstream.go79-87 intra/dns53/upstream.go140-147
The underlying dns.Client is configured with Net: "udp". If UDP fails or a large response is expected, connect() manages the connection logic, including proxy selection via pxdial.
Sources: intra/dns53/upstream.go126-134 intra/dns53/upstream.go149-195 intra/dns53/upstream.go221-284
Connection pooling exists via core.MultConnPool[uint64] keyed by proxy handle, but is currently disabled (usepool: false) due to stability concerns with pooled WireGuard DNS connections.
Sources: intra/dns53/upstream.go113-115 intra/dns53/upstream.go198-205
Package: intra/dnscrypt
Structs: DcMulti (implements dnsx.TransportMult), server (implements dnsx.Transport)
Type constant: dnsx.DNSCrypt
DNSCrypt is a multiplexed transport containing multiple server endpoints and optional anonymous relays.
Sources: intra/dnscrypt/multiserver.go45-62 intra/dnscrypt/servers.go49-71 intra/dnscrypt/servers.go75-79
encrypt() constructed the DNSCrypt query with the shared key and handles padding.udpExchange() or tcpExchange() sends the query.prepareForRelay() prepends the anonymized DNS header (RFC-style relaying).decrypt() validates the response magic, nonce, and tag.Sources: intra/dnscrypt/multiserver.go92-144 intra/dnscrypt/multiserver.go146-189 intra/dnscrypt/multiserver.go191-199 intra/dnscrypt/multiserver.go201-305
Struct: dnssd
The dnssd transport always uses ipn.Exit as its dialer. It binds to the standard mDNS multicast addresses 224.0.0.251:5353 (IPv4) or [ff02::fb]:5353 (IPv6). It executes "oneshot" queries by sending to the multicast group and listening for unicast responses.
Sources: intra/dns53/mdns.go60-84 intra/dns53/mdns.go112-158 intra/dns53/mdns.go223-228
Struct: goosr
Uses the Go native net.Resolver with PreferGo: true and a custom pxdial that routes through the exit proxy. It supports both pure Go and cgo-based resolution depending on the Loopingback setting.
Sources: intra/dns53/goos.go42-73 intra/dns53/goos.go75-80 intra/dns53/goos.go120-134
Both the DoH transport and the DoT dot structs support ECH. They periodically attempt to fetch ECH configurations (e.g., from SVCB/HTTPS records) and store them in volatile fields.
| Field | Type | Purpose |
|---|---|---|
echconfig | atomic.Pointer[tls.Config] | Cached ECH-enabled TLS config |
echlastattempt | atomic.Int64 | Throttle retry (8-hour echRetryPeriod) |
Sources: intra/doh/doh.go99-101 intra/dns53/dot.go55-56 intra/doh/doh.go68 intra/dns53/dot.go33
Every transport holds an ipn.ProxyProvider and a relay string. The relay is pre-resolved at construction time from px.ProxyFor(id).
The system uses a structured QueryError type to categorize failures, which are then mapped to human-readable strings for logging and UI.
| Status | String | Use Case |
|---|---|---|
Complete | "OK" | Successful resolution |
BadQuery | "Bad Query" | Client-side query formatting error |
TransportError | "Refusing" | HTTP 5xx or connection failures |
ClientError | "Missing" | HTTP 4xx errors |
Sources: intra/dnsx/queryerror.go14-59 intra/doh/doh.go763-776 intra/dns53/upstream.go149-182
Refresh this wiki