/* * Copyright (c) 2022, Psiphon Inc. * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ // Package resolver implements a DNS stub resolver, or DNS client, which // resolves domain names. // // The resolver is Psiphon-specific and oriented towards blocking resistance. // See ResolveIP for more details. package resolver import ( "context" "encoding/hex" "fmt" "net" "sync" "sync/atomic" "syscall" "time" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/parameters" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/prng" "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms" lrucache "github.com/cognusion/go-cache-lru" "github.com/miekg/dns" ) const ( resolverCacheDefaultTTL = 1 * time.Minute resolverCacheReapFrequency = 1 * time.Minute resolverCacheMaxEntries = 10000 resolverServersUpdateTTL = 5 * time.Second resolverDefaultAttemptsPerServer = 2 resolverDefaultRequestTimeout = 5 * time.Second resolverDefaultAwaitTimeout = 10 * time.Millisecond resolverDefaultAnswerTTL = 1 * time.Minute resolverDNSPort = "53" udpPacketBufferSize = 1232 ) // NetworkConfig specifies network-level configuration for a Resolver. type NetworkConfig struct { // GetDNSServers returns a list of system DNS server addresses (IP:port, or // IP only with port 53 assumed), as determined via OS APIs, in priority // order. GetDNSServers may be nil. GetDNSServers func() []string // BindToDevice should ensure the input file descriptor, a UDP socket, is // excluded from VPN routing. BindToDevice may be nil. BindToDevice func(fd int) (string, error) // AllowDefaultResolverWithBindToDevice indicates that it's safe to use // the default resolver when BindToDevice is configured, as the host OS // will automatically exclude DNS requests from the VPN. AllowDefaultResolverWithBindToDevice bool // IPv6Synthesize should apply NAT64 synthesis to the input IPv4 address, // returning a synthesized IPv6 address that will route to the same // endpoint. IPv6Synthesize may be nil. IPv6Synthesize func(IPv4 string) string // HasIPv6Route should return true when the host has an IPv6 route. // Resolver has an internal implementation, hasRoutableIPv6Interface, to // determine this, but it can fail on some platforms ("route ip+net: // netlinkrib: permission denied" on Android, for example; see Go issue // 40569). When HasIPv6Route is nil, the internal implementation is used. HasIPv6Route func() bool // LogWarning is an optional callback which is used to log warnings and // transient errors which would otherwise not be recorded or returned. LogWarning func(error) // LogHostnames indicates whether to log hostname in errors or not. LogHostnames bool // CacheExtensionInitialTTL specifies a minimum TTL to use when caching // domain resolution results. This minimum will override any TTL in the // DNS response. CacheExtensionInitialTTL is off when 0. CacheExtensionInitialTTL time.Duration // CacheExtensionVerifiedTTL specifies the minimum TTL to set for a cached // domain resolution result after the result has been verified. // CacheExtensionVerifiedTTL is off when 0. // // DNS cache extension is a workaround to partially mitigate issues with // obtaining underlying system DNS server IPs on platforms such as iOS // once a VPN is running and after network changes, such as changing from // Wi-Fi to mobile. While ResolveParameters.AlternateDNSServer can be // used to specify a known public DNS server, it may be the case that // public DNS servers are blocked or always falling back to a public DNS // server creates unusual traffic. And while it may be possible to use // the default system resolver, it lacks certain circumvention // capabilities. // // Extending the TTL for cached responses allows Psiphon to redial domains // using recently successful IPs. // // CacheExtensionInitialTTL allows for a greater initial minimum TTL, so // that the response entry remains in the cache long enough for a dial to // fully complete and verify the endpoint. Psiphon will call // Resolver.VerifyExtendCacheTTL once a dial has authenticated, for // example, the destination Psiphon server. VerifyCacheExtension will // further extend the corresponding TTL to CacheExtensionVerifiedTTL, a // longer TTL. CacheExtensionInitialTTL is intended to be on the order of // minutes and CacheExtensionVerifiedTTL may be on the order of hours. // // When CacheExtensionVerifiedTTL is on, the DNS cache is not flushed on // network changes, to allow for the previously cached entries to remain // available in the problematic scenario. Like adjusting TTLs, this is an // explicit trade-off which doesn't adhere to standard best practise, but // is expected to be more blocking resistent; this approach also assumes // that endpoints such as CDN IPs are typically available on any network. CacheExtensionVerifiedTTL time.Duration } func (c *NetworkConfig) allowDefaultResolver() bool { // When BindToDevice is configured, the standard library resolver is not // used, as the system resolver may not route outside of the VPN. return c.BindToDevice == nil || c.AllowDefaultResolverWithBindToDevice } func (c *NetworkConfig) logWarning(err error) { if c.LogWarning != nil { c.LogWarning(err) } } // ResolveParameters specifies the configuration and behavior of a single // ResolveIP call, a single domain name resolution. // // New ResolveParameters may be generated by calling MakeResolveParameters, // which takes tactics parameters as an input. // // ResolveParameters may be persisted for replay. type ResolveParameters struct { // AttemptsPerServer specifies how many requests to send to each DNS // server before trying the next server. IPv4 and IPv6 requests are sent // concurrently and count as one attempt. AttemptsPerServer int // AttemptsPerPreferredServer is AttemptsPerServer for a preferred // alternate DNS server. AttemptsPerPreferredServer int // RequestTimeout specifies how long to wait for a valid response before // moving on to the next attempt. RequestTimeout time.Duration // AwaitTimeout specifies how long to await an additional response after // the first response is received. This additional wait time applies only // when there is either no IPv4 or IPv6 response. AwaitTimeout time.Duration // PreresolvedIPAddress specifies an IP address result to be used in place // of making a request. PreresolvedIPAddress string // AlternateDNSServer specifies an alterate DNS server (IP:port, or IP // only with port 53 assumed) to be used when either no system DNS // servers are available or when PreferAlternateDNSServer is set. AlternateDNSServer string // PreferAlternateDNSServer indicates whether to prioritize using the // AlternateDNSServer. When set, the AlternateDNSServer is attempted // before any system DNS servers. PreferAlternateDNSServer bool // ProtocolTransformName specifies the name associated with // ProtocolTransformSpec and is used for metrics. ProtocolTransformName string // ProtocolTransformSpec specifies a transform to apply to the DNS request packet. // See: "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/transforms". // // As transforms operate on strings and DNS requests are binary, // transforms should be expressed using hex characters. // // DNS transforms include strategies discovered by the Geneva team, // https://geneva.cs.umd.edu. ProtocolTransformSpec transforms.Spec // ProtocolTransformSeed specifies the seed to use for generating random // data in the ProtocolTransformSpec transform. To replay a transform, // specify the same seed. ProtocolTransformSeed *prng.Seed // IncludeEDNS0 indicates whether to include the EDNS(0) UDP maximum // response size extension in DNS requests. The resolver can handle // responses larger than 512 bytes (RFC 1035 maximum) regardless of // whether the extension is included; the extension may be included as // part of appearing similar to other DNS traffic. IncludeEDNS0 bool firstAttemptWithAnswer int32 } // GetFirstAttemptWithAnswer returns the index of the first request attempt // that received a valid response, for the most recent ResolveIP call using // this ResolveParameters. This information is used for logging metrics. The // first attempt has index 1. GetFirstAttemptWithAnswer return 0 when no // request attempt has reported a valid response. // // The caller is responsible for synchronizing use of a ResolveParameters // instance (e.g, use a distinct ResolveParameters per ResolveIP to ensure // GetFirstAttemptWithAnswer refers to a specific ResolveIP). func (r *ResolveParameters) GetFirstAttemptWithAnswer() int { return int(atomic.LoadInt32(&r.firstAttemptWithAnswer)) } func (r *ResolveParameters) setFirstAttemptWithAnswer(attempt int) { atomic.StoreInt32(&r.firstAttemptWithAnswer, int32(attempt)) } // Implementation note: Go's standard net.Resolver supports specifying a // custom Dial function. This could be used to implement at least a large // subset of the Resolver functionality on top of Go's standard library // resolver. However, net.Resolver is limited to using the CGO resolver on // Android, https://github.com/golang/go/issues/8877, in which case the // custom Dial function is not used. Furthermore, the the pure Go resolver in // net/dnsclient_unix.go appears to not be used on Windows at this time. // // Go also provides golang.org/x/net/dns/dnsmessage, a DNS message marshaller, // which could potentially be used in place of github.com/miekg/dns. // Resolver is a DNS stub resolver, or DNS client, which resolves domain // names. A Resolver instance maintains a cache, a network state snapshot, // and metrics. All ResolveIP calls will share the same cache and state. // Multiple concurrent ResolveIP calls are supported. type Resolver struct { networkConfig *NetworkConfig mutex sync.Mutex networkID string hasIPv6Route bool systemServers []string lastServersUpdate time.Time cache *lrucache.Cache metrics resolverMetrics } type resolverMetrics struct { resolves int cacheHits int verifiedCacheExtensions int requestsIPv4 int requestsIPv6 int responsesIPv4 int responsesIPv6 int defaultResolves int defaultSuccesses int peakInFlight int64 minRTT time.Duration maxRTT time.Duration } func newResolverMetrics() resolverMetrics { return resolverMetrics{minRTT: -1} } // NewResolver creates a new Resolver instance. func NewResolver(networkConfig *NetworkConfig, networkID string) *Resolver { r := &Resolver{ networkConfig: networkConfig, metrics: newResolverMetrics(), } // updateNetworkState will initialize the cache and network state, // including system DNS servers. r.updateNetworkState(networkID) return r } // Stop clears the Resolver cache and resets metrics. Stop must be called only // after ceasing all in-flight ResolveIP goroutines, or else the cache or // metrics may repopulate. A Resolver may be resumed after calling Stop, but // Update must be called first. func (r *Resolver) Stop() { r.mutex.Lock() defer r.mutex.Unlock() // r.networkConfig is not set to nil to avoid possible nil pointer // dereferences by concurrent ResolveIP calls. r.networkID = "" r.hasIPv6Route = false r.systemServers = nil r.cache.Flush() r.metrics = newResolverMetrics() } // MakeResolveParameters generates ResolveParameters using the input tactics // parameters and optional frontingProviderID context. func (r *Resolver) MakeResolveParameters( p parameters.ParametersAccessor, frontingProviderID string) (*ResolveParameters, error) { params := &ResolveParameters{ AttemptsPerServer: p.Int(parameters.DNSResolverAttemptsPerServer), AttemptsPerPreferredServer: p.Int(parameters.DNSResolverAttemptsPerPreferredServer), RequestTimeout: p.Duration(parameters.DNSResolverRequestTimeout), AwaitTimeout: p.Duration(parameters.DNSResolverAwaitTimeout), } // When a frontingProviderID is specified, generate a pre-resolved IP // address, based on tactics configuration. if frontingProviderID != "" { if p.WeightedCoinFlip(parameters.DNSResolverPreresolvedIPAddressProbability) { CIDRs := p.LabeledCIDRs(parameters.DNSResolverPreresolvedIPAddressCIDRs, frontingProviderID) if len(CIDRs) > 0 { CIDR := CIDRs[prng.Intn(len(CIDRs))] IP, err := generateIPAddressFromCIDR(CIDR) if err != nil { return nil, errors.Trace(err) } params.PreresolvedIPAddress = IP.String() } } } // When PreresolvedIPAddress is set, there's no DNS request and the // following params can be skipped. if params.PreresolvedIPAddress != "" { return params, nil } // When preferring an alternate DNS server, select the alternate from // DNSResolverPreferredAlternateServers. This list is for circumvention // operations, such as using a public DNS server with a protocol // transform. Otherwise, select from DNSResolverAlternateServers, which // is a fallback list of DNS servers to be used when the system DNS // servers cannot be obtained. preferredServers := p.Strings(parameters.DNSResolverPreferredAlternateServers) preferAlternateDNSServer := len(preferredServers) > 0 && p.WeightedCoinFlip( parameters.DNSResolverPreferAlternateServerProbability) alternateServers := preferredServers if !preferAlternateDNSServer { alternateServers = p.Strings(parameters.DNSResolverAlternateServers) } // Select an alternate DNS server, typically a public DNS server. Ensure // tactics is configured with an empty DNSResolverAlternateServers list // in cases where attempts to public DNS server are unwanted. if len(alternateServers) > 0 { alternateServer := alternateServers[prng.Intn(len(alternateServers))] // Check that the alternateServer has a well-formed IP address; and add // a default port if none it present. host, _, err := net.SplitHostPort(alternateServer) if err != nil { // Assume the SplitHostPort error is due to missing port. host = alternateServer alternateServer = net.JoinHostPort(alternateServer, resolverDNSPort) } if net.ParseIP(host) == nil { // Log warning and proceed without this DNS server. r.networkConfig.logWarning( errors.TraceNew("invalid alternate DNS server IP address")) } else { params.AlternateDNSServer = alternateServer params.PreferAlternateDNSServer = preferAlternateDNSServer } } // Select a DNS transform. DNS request transforms are "scoped" by // alternate DNS server (IP address without port); that is, when an // alternate DNS server is certain to be attempted first, a transform // associated with and known to work with that DNS server will be // selected. Otherwise, a transform from the default scope // (transforms.SCOPE_ANY == "") is selected. // // In any case, ResolveIP will only apply a transform on the first request // attempt. if p.WeightedCoinFlip(parameters.DNSResolverProtocolTransformProbability) { specs := p.ProtocolTransformSpecs( parameters.DNSResolverProtocolTransformSpecs) scopedSpecNames := p.ProtocolTransformScopedSpecNames( parameters.DNSResolverProtocolTransformScopedSpecNames) // The alternate DNS server will be the first attempt if // PreferAlternateDNSServer or the list of system DNS servers is empty. // // Limitation: the system DNS server list may change, due to a later // Resolver.update call when ResolveIP is called with these // ResolveParameters. _, systemServers := r.getNetworkState() scope := transforms.SCOPE_ANY if params.AlternateDNSServer != "" && (params.PreferAlternateDNSServer || len(systemServers) == 0) { // Remove the port number, as the scope key is an IP address only. // // TODO: when we only just added the default port above, which is // the common case, we could avoid this extra split. host, _, err := net.SplitHostPort(params.AlternateDNSServer) if err != nil { return nil, errors.Trace(err) } scope = host } name, spec := specs.Select(scope, scopedSpecNames) if spec != nil { params.ProtocolTransformName = name params.ProtocolTransformSpec = spec var err error params.ProtocolTransformSeed, err = prng.NewSeed() if err != nil { return nil, errors.Trace(err) } } } if p.WeightedCoinFlip(parameters.DNSResolverIncludeEDNS0Probability) { params.IncludeEDNS0 = true } return params, nil } // ResolveAddress splits the input host:port address, calls ResolveIP to // resolve the IP address of the host, selects an IP if there are multiple, // and returns a rejoined IP:port. func (r *Resolver) ResolveAddress( ctx context.Context, networkID string, params *ResolveParameters, address string) (string, error) { hostname, port, err := net.SplitHostPort(address) if err != nil { return "", errors.Trace(err) } IPs, err := r.ResolveIP(ctx, networkID, params, hostname) if err != nil { return "", errors.Trace(err) } return net.JoinHostPort(IPs[prng.Intn(len(IPs))].String(), port), nil } // ResolveIP resolves a domain name. // // The input params may be nil, in which case default timeouts are used. // // ResolveIP performs concurrent A and AAAA lookups, returns any valid // response IPs, and caches results. An error is returned when there are // no valid response IPs. // // ResolveIP is not a general purpose resolver and is Psiphon-specific. For // example, resolved domains are expected to exist; ResolveIP does not // fallback to TCP; does not consult any "hosts" file; does not perform RFC // 3484 sorting logic (see Go issue 18518); only implements a subset of // Go/glibc/resolv.conf(5) resolver parameters (attempts and timeouts, but // not rotate, single-request etc.) ResolveIP does not implement singleflight // logic, as the Go resolver does, and allows multiple concurrent request for // the same domain -- Psiphon won't often resolve the exact same domain // multiple times concurrently, and, when it does, there's a circumvention // benefit to attempting different DNS servers and protocol transforms. // // ResolveIP does not currently support DoT, DoH, or TCP; those protocols are // often blocked or less common. Instead, ResolveIP makes a best effort to // evade plaintext UDP DNS interference by ignoring invalid responses and by // optionally applying protocol transforms that may evade blocking. func (r *Resolver) ResolveIP( ctx context.Context, networkID string, params *ResolveParameters, hostname string) ([]net.IP, error) { // ResolveIP does _not_ lock r.mutex for the lifetime of the function, to // ensure many ResolveIP calls can run concurrently. // If the hostname is already an IP address, just return that. For // metrics, this does not count as a resolve, as the caller may invoke // ResolveIP for all dials. IP := net.ParseIP(hostname) if IP != nil { return []net.IP{IP}, nil } // Count all resolves of an actual domain, including cached and // pre-resolved cases. r.updateMetricResolves() // Call updateNetworkState immediately before resolving, as a best effort // to ensure that system DNS servers and IPv6 routing network state // reflects the current network. updateNetworkState locks the Resolver // mutex for its duration, and so concurrent ResolveIP calls may block at // this point. However, all updateNetworkState operations are local to // the host or device; and, if the networkID is unchanged since the last // call, updateNetworkState may not perform any operations; and after the // updateNetworkState call, ResolveIP proceeds without holding the mutex // lock. As a result, this step should not prevent ResolveIP concurrency. r.updateNetworkState(networkID) if params == nil { // Supply default ResolveParameters params = &ResolveParameters{ AttemptsPerServer: resolverDefaultAttemptsPerServer, AttemptsPerPreferredServer: resolverDefaultAttemptsPerServer, RequestTimeout: resolverDefaultRequestTimeout, AwaitTimeout: resolverDefaultAwaitTimeout, } } // When PreresolvedIPAddress is set, tactics parameters determined the IP address // in this case. if params.PreresolvedIPAddress != "" { IP := net.ParseIP(params.PreresolvedIPAddress) if IP == nil { // Unexpected case, as MakeResolveParameters selects the IP address. return nil, errors.TraceNew("invalid IP address") } return []net.IP{IP}, nil } // Use a snapshot of the current network state, including IPv6 routing and // system DNS servers. // // Limitation: these values are used even if the network changes in the // middle of a ResolveIP call; ResolveIP is not interrupted if the // network changes. hasIPv6Route, systemServers := r.getNetworkState() // Use the standard library resolver when there's no GetDNSServers, or the // system server list is otherwise empty, and no alternate DNS server is // configured. // // Note that in the case where there are no system DNS servers and there // is an AlternateDNSServer, if the AlternateDNSServer attempt fails, // control does not flow back to defaultResolverLookupIP. On platforms // without GetDNSServers, the caller must arrange for distinct attempts // that try a AlternateDNSServer, or just use the standard library // resolver. // // ResolveIP should always be called, even when defaultResolverLookupIP is // expected to be used, to ensure correct metrics counts and ensure a // consistent error message log stack for all DNS-related failures. // if len(systemServers) == 0 && params.AlternateDNSServer == "" && r.networkConfig.allowDefaultResolver() { IPs, err := defaultResolverLookupIP(ctx, hostname, r.networkConfig.LogHostnames) r.updateMetricDefaultResolver(err == nil) if err != nil { return nil, errors.Trace(err) } return IPs, err } // Consult the cache before making queries. This comes after the standard // library case, to allow the standard library to provide its own caching // logic. IPs := r.getCache(hostname) if IPs != nil { return IPs, nil } // Set the list of DNS servers to attempt. AlternateDNSServer is used // first when PreferAlternateDNSServer is set; otherwise // AlternateDNSServer is used only when there is no system DNS server. var servers []string if params.AlternateDNSServer != "" && (len(systemServers) == 0 || params.PreferAlternateDNSServer) { servers = []string{params.AlternateDNSServer} } servers = append(servers, systemServers...) if len(servers) == 0 { return nil, errors.TraceNew("no DNS servers") } // Set the request timeout and set up a reusable timer for handling // request and await timeouts. // // We expect to always have a request timeout. Handle the unexpected no // timeout, 0, case by setting the longest timeout possible, ~290 years; // always having a non-zero timeout makes the following code marginally // simpler. requestTimeout := params.RequestTimeout if requestTimeout == 0 { requestTimeout = 1<<63 - 1 } var timer *time.Timer timerDrained := true resetTimer := func(timeout time.Duration) { if timer == nil { timer = time.NewTimer(timeout) } else { if !timerDrained && !timer.Stop() { <-timer.C } timer.Reset(timeout) } timerDrained = false } // Orchestrate the DNS requests resolveCtx, cancelFunc := context.WithCancel(ctx) defer cancelFunc() waitGroup := new(sync.WaitGroup) conns := common.NewConns() type answer struct { attempt int IPs []net.IP TTLs []time.Duration } var maxAttempts int if params.PreferAlternateDNSServer { maxAttempts = params.AttemptsPerPreferredServer maxAttempts += (len(servers) - 1) * params.AttemptsPerServer } else { maxAttempts = len(servers) * params.AttemptsPerServer } answerChan := make(chan *answer, maxAttempts*2) inFlight := int64(0) awaitA := int32(1) awaitAAAA := int32(1) if !hasIPv6Route { awaitAAAA = 0 } var result *answer var lastErr atomic.Value stop := false for i := 0; !stop && i < maxAttempts; i++ { var index int if params.PreferAlternateDNSServer { if i < params.AttemptsPerPreferredServer { index = 0 } else { index = 1 + ((i - params.AttemptsPerPreferredServer) / params.AttemptsPerServer) } } else { index = i / params.AttemptsPerServer } server := servers[index] // Only the first attempt pair tries transforms, as it's not certain // the transforms will be compatible with DNS servers. useProtocolTransform := (i == 0 && params.ProtocolTransformSpec != nil) // Send A and AAAA requests concurrently. questionTypes := []resolverQuestionType{resolverQuestionTypeA, resolverQuestionTypeAAAA} if !hasIPv6Route { questionTypes = questionTypes[0:1] } for _, questionType := range questionTypes { waitGroup.Add(1) // For metrics, track peak concurrent in-flight requests for // a _single_ ResolveIP. inFlight for this ResolveIP is also used // to determine whether to await additional responses once the // first, valid response is received. For that logic to be // correct, we must increment inFlight in this outer goroutine to // ensure the await logic sees either inFlight > 0 or an answer // in the channel. r.updateMetricPeakInFlight(atomic.AddInt64(&inFlight, 1)) go func(attempt int, questionType resolverQuestionType, useProtocolTransform bool) { defer waitGroup.Done() // We must decrement inFlight only after sending an answer and // setting awaitA or awaitAAAA to ensure that the await logic // in the outer goroutine will see inFlight 0 only once those // operations are complete. // // We cannot wait and decrement inFlight when the outer // goroutine receives answers, as no answer is sent in some // cases, such as when the resolve fails due to NXDOMAIN. defer atomic.AddInt64(&inFlight, -1) // The request count metric counts the _intention_ to send // requests, as there's a possibility that newResolverConn or // performDNSQuery fail locally before sending a request packet. switch questionType { case resolverQuestionTypeA: r.updateMetricRequestsIPv4() case resolverQuestionTypeAAAA: r.updateMetricRequestsIPv6() } // While it's possible, and potentially more optimal, to use // the same UDP socket for both the A and AAAA request, we // use a distinct socket per request, as common DNS clients do. conn, err := r.newResolverConn(r.networkConfig.logWarning, server) if err != nil { lastErr.Store(errors.Trace(err)) return } defer conn.Close() // There's no context.Context support in the underlying API // used by performDNSQuery, so instead collect all the // request conns so that they can be closed, and any blocking // network I/O interrupted, below, if resolveCtx is done. if !conns.Add(conn) { // Add fails when conns is already closed. return } // performDNSQuery will send the request and read a response. // performDNSQuery will continue reading responses until it // receives a valid response, which can mitigate a subset of // DNS injection attacks (to the limited extent possible for // plaintext DNS). // // For IPv4, NXDOMAIN or a response with no IPs is not // expected for domains resolved by Psiphon, so // performDNSQuery treats such a response as invalid. For // IPv6, a response with no IPs, may be valid(even though the // response could be forged); the resolver will continue its // attempts loop if it has no other IPs. // // Each performDNSQuery has no timeout and runs // until it has read a valid response or the requestCtx is // done. This allows for slow arriving, valid responses to // eventually succeed, even if the read time exceeds // requestTimeout, as long as the read time is less than the // requestCtx timeout. // // With this approach, the overall ResolveIP call may have // more than 2 performDNSQuery requests in-flight at a time, // as requestTimeout is used to schedule sending the next // attempt but not cancel the current attempt. For // connectionless UDP, the resulting network traffic should // be similar to common DNS clients which do cancel request // before beginning the next attempt. IPs, TTLs, RTT, err := performDNSQuery( resolveCtx, r.networkConfig.logWarning, params, useProtocolTransform, conn, questionType, hostname) // Update the min/max RTT metric when reported (>=0) even if // the result is an error; i.e., the even if there was an // invalid response. // // Limitation: since individual requests aren't cancelled // after requestTimeout, RTT metrics won't reflect // no-response cases, although request and response count // disparities will still show up in the metrics. if RTT >= 0 { r.updateMetricRTT(RTT) } if err != nil { lastErr.Store(errors.Trace(err)) return } if len(IPs) > 0 { select { case answerChan <- &answer{attempt: attempt, IPs: IPs, TTLs: TTLs}: default: } } // Mark no longer awaiting A or AAAA as long as there is a // valid response, even if there are no IPs in the IPv6 case. switch questionType { case resolverQuestionTypeA: r.updateMetricResponsesIPv4() atomic.StoreInt32(&awaitA, 0) case resolverQuestionTypeAAAA: r.updateMetricResponsesIPv6() atomic.StoreInt32(&awaitAAAA, 0) default: } }(i+1, questionType, useProtocolTransform) } resetTimer(requestTimeout) select { case result = <-answerChan: // When the first answer, a response with valid IPs, arrives, exit // the attempts loop. The following await branch may collect // additional answers. params.setFirstAttemptWithAnswer(result.attempt) stop = true case <-timer.C: // When requestTimeout arrives, loop around and launch the next // attempt; leave the existing requests running in case they // eventually respond. timerDrained = true case <-resolveCtx.Done(): // When resolveCtx is done, exit the attempts loop. // // Append the existing lastErr, which may convey useful // information to be reported in a failed_tunnel error message. lastErr.Store(errors.Tracef("%v (lastErr: %v)", ctx.Err(), lastErr.Load())) stop = true } } // Receive any additional answers, now present in the channel, which // arrived concurrent with the first answer. This receive avoids a race // condition where inFlight may now be 0, with additional answers // enqueued, in which case the following await branch is not taken. // // It's possible for the attempts loop to exit with no received answer due // to timeouts or cancellation while, concurrently, an answer is sent to // the channel. In this case, when result == nil, we ignore the answers // and leave this as a failed resolve. if result != nil { for loop := true; loop; { select { case nextAnswer := <-answerChan: result.IPs = append(result.IPs, nextAnswer.IPs...) result.TTLs = append(result.TTLs, nextAnswer.TTLs...) default: loop = false } } } // When we have an answer, await -- for a short time, // params.AwaitTimeout -- extra answers from any remaining in-flight // requests. Only await if the request isn't cancelled and we don't // already have at least one IPv4 and one IPv6 response; only await AAAA // if it was sent; note that a valid AAAA response may include no IPs // lastErr is not set in timeout/cancelled cases here, since we already // have an answer. if result != nil && resolveCtx.Err() == nil && atomic.LoadInt64(&inFlight) > 0 && (atomic.LoadInt32(&awaitA) != 0 || atomic.LoadInt32(&awaitAAAA) != 0) && params.AwaitTimeout > 0 { resetTimer(params.AwaitTimeout) for { stop := false select { case nextAnswer := <-answerChan: result.IPs = append(result.IPs, nextAnswer.IPs...) result.TTLs = append(result.TTLs, nextAnswer.TTLs...) case <-timer.C: timerDrained = true stop = true case <-resolveCtx.Done(): stop = true } if stop || atomic.LoadInt64(&inFlight) == 0 || (atomic.LoadInt32(&awaitA) == 0 && atomic.LoadInt32(&awaitAAAA) == 0) { break } } } timer.Stop() // Interrupt all workers. cancelFunc() conns.CloseAll() waitGroup.Wait() // When there's no answer, return the last error. if result == nil { err := lastErr.Load() if err == nil { err = errors.TraceNew("unexpected missing error") } if r.networkConfig.LogHostnames { err = fmt.Errorf("resolve %s : %w", hostname, err.(error)) } return nil, errors.Trace(err.(error)) } if len(result.IPs) == 0 { // Unexpected, since a len(IPs) > 0 check precedes sending to answerChan. return nil, errors.TraceNew("unexpected no IPs") } // Update the cache now, after all results are gathered. r.setCache(hostname, result.IPs, result.TTLs) return result.IPs, nil } // VerifyCacheExtension extends the TTL for any cached result for the // specified hostname to at least NetworkConfig.CacheExtensionVerifiedTTL. func (r *Resolver) VerifyCacheExtension(hostname string) { r.mutex.Lock() defer r.mutex.Unlock() if r.networkConfig.CacheExtensionVerifiedTTL == 0 { return } if net.ParseIP(hostname) != nil { return } entry, expires, ok := r.cache.GetWithExpiration(hostname) if !ok { return } // Change the TTL only if the entry expires and the existing TTL isn't // longer than the extension. neverExpires := time.Time{} if expires == neverExpires || expires.After(time.Now().Add(r.networkConfig.CacheExtensionVerifiedTTL)) { return } r.cache.Set(hostname, entry, r.networkConfig.CacheExtensionVerifiedTTL) r.metrics.verifiedCacheExtensions += 1 } // GetMetrics returns a summary of DNS metrics. func (r *Resolver) GetMetrics() string { r.mutex.Lock() defer r.mutex.Unlock() // When r.metrics.minRTT < 0, min/maxRTT is unset. minRTT := "n/a" maxRTT := minRTT if r.metrics.minRTT >= 0 { minRTT = fmt.Sprintf("%d", r.metrics.minRTT/time.Millisecond) maxRTT = fmt.Sprintf("%d", r.metrics.maxRTT/time.Millisecond) } extend := "" if r.networkConfig.CacheExtensionVerifiedTTL > 0 { extend = fmt.Sprintf("| extend %d ", r.metrics.verifiedCacheExtensions) } defaultResolves := "" if r.networkConfig.allowDefaultResolver() { defaultResolves = fmt.Sprintf( " | def %d/%d", r.metrics.defaultResolves, r.metrics.defaultSuccesses) } // Note that the number of system resolvers is a point-in-time value, // while the others are cumulative. return fmt.Sprintf("resolves %d | hit %d %s| req v4/v6 %d/%d | resp %d/%d | peak %d | rtt %s - %s ms. | sys %d%s", r.metrics.resolves, r.metrics.cacheHits, extend, r.metrics.requestsIPv4, r.metrics.requestsIPv6, r.metrics.responsesIPv4, r.metrics.responsesIPv6, r.metrics.peakInFlight, minRTT, maxRTT, len(r.systemServers), defaultResolves) } // updateNetworkState updates the system DNS server list, IPv6 state, and the // cache. // // Any errors that occur while querying network state are logged; in error // conditions the functionality of the resolver may be reduced, but the // resolver remains operational. func (r *Resolver) updateNetworkState(networkID string) { r.mutex.Lock() defer r.mutex.Unlock() // Only perform blocking/expensive update operations when necessary. updateAll := false updateIPv6Route := false updateServers := false flushCache := false // If r.cache is nil, this is the first update call in NewResolver. Create // the cache and perform all updates. if r.cache == nil { r.cache = lrucache.NewWithLRU( resolverCacheDefaultTTL, resolverCacheReapFrequency, resolverCacheMaxEntries) updateAll = true } // Perform all updates when the networkID has changed, which indicates a // different network. if r.networkID != networkID { updateAll = true } if updateAll { updateIPv6Route = true updateServers = true flushCache = true } // Even when the networkID has not changed, update DNS servers // periodically. This is similar to how other DNS clients // poll /etc/resolv.conf, including the period of 5s. if time.Since(r.lastServersUpdate) > resolverServersUpdateTTL { updateServers = true } // Update hasIPv6Route, which indicates whether the current network has an // IPv6 route and so if DNS requests for AAAA records will be sent. // There's no use for AAAA records on IPv4-only networks; and other // common DNS clients omit AAAA requests on IPv4-only records, so these // requests would otherwise be unusual. // // There's no hasIPv4Route as we always need to resolve A records, // particularly for IPv4-only endpoints; for IPv6-only networks, // NetworkConfig.IPv6Synthesize should be used to accomodate IPv4 DNS // server addresses, and dials performed outside the Resolver will // similarly use NAT 64 (on iOS; on Android, 464XLAT will handle this // transparently). if updateIPv6Route { if r.networkConfig.HasIPv6Route != nil { r.hasIPv6Route = r.networkConfig.HasIPv6Route() } else { hasIPv6Route, err := hasRoutableIPv6Interface() if err != nil { // Log warning and proceed without IPv6. r.networkConfig.logWarning( errors.Tracef("unable to determine IPv6 route: %v", err)) hasIPv6Route = false } r.hasIPv6Route = hasIPv6Route } } // Update the list of system DNS servers. It's not an error condition here // if the list is empty: a subsequent ResolveIP may use // ResolveParameters which specifies an AlternateDNSServer. if updateServers && r.networkConfig.GetDNSServers != nil { systemServers := []string{} for _, systemServer := range r.networkConfig.GetDNSServers() { host, _, err := net.SplitHostPort(systemServer) if err != nil { // Assume the SplitHostPort error is due to systemServer being // an IP only, and append the default port, 53. If // systemServer _isn't_ an IP, the following ParseIP will fail. host = systemServer systemServer = net.JoinHostPort(systemServer, resolverDNSPort) } if net.ParseIP(host) == nil { // Log warning and proceed without this DNS server. r.networkConfig.logWarning( errors.TraceNew("invalid DNS server IP address")) continue } systemServers = append(systemServers, systemServer) } // Check if the list of servers has changed, including order. If // changed, flush the cache even if the networkID has not changed. // Cached results are only considered valid as long as the system DNS // configuration remains the same. equal := len(r.systemServers) == len(systemServers) if equal { for i := 0; i < len(r.systemServers); i++ { if r.systemServers[i] != systemServers[i] { equal = false break } } } flushCache = flushCache || !equal // Concurrency note: once the r.systemServers slice is set, the // contents of the backing array must not be modified due to // concurrent ResolveIP calls. r.systemServers = systemServers r.lastServersUpdate = time.Now() } // Skip cache flushes when the extended DNS caching mechanism is enabled. // TODO: retain only verified cache entries? if flushCache && r.networkConfig.CacheExtensionVerifiedTTL == 0 { r.cache.Flush() } // Set r.networkID only after all operations complete without errors; if // r.networkID were set earlier, a subsequent // ResolveIP/updateNetworkState call might proceed as if the network // state were updated for the specified network ID. r.networkID = networkID } func (r *Resolver) getNetworkState() (bool, []string) { r.mutex.Lock() defer r.mutex.Unlock() return r.hasIPv6Route, r.systemServers } func (r *Resolver) setCache(hostname string, IPs []net.IP, TTLs []time.Duration) { r.mutex.Lock() defer r.mutex.Unlock() // The shortest TTL is used. In some cases, a DNS server may omit the TTL // or set a 0 TTL, in which case the default is used. TTL := resolverDefaultAnswerTTL for _, answerTTL := range TTLs { if answerTTL > 0 && answerTTL < TTL { TTL = answerTTL } } // When NetworkConfig.CacheExtensionInitialTTL configured, ensure the TTL // is no shorter than CacheExtensionInitialTTL. if r.networkConfig.CacheExtensionInitialTTL != 0 && TTL < r.networkConfig.CacheExtensionInitialTTL { TTL = r.networkConfig.CacheExtensionInitialTTL } // Limitation: with concurrent ResolveIPs for the same domain, the last // setCache call determines the cache value. The results are not merged. r.cache.Set(hostname, IPs, TTL) } func (r *Resolver) getCache(hostname string) []net.IP { r.mutex.Lock() defer r.mutex.Unlock() entry, ok := r.cache.Get(hostname) if !ok { return nil } r.metrics.cacheHits += 1 return entry.([]net.IP) } // newResolverConn creates a UDP socket that will send packets to serverAddr. // serverAddr is an IP:port, which allows specifying the port for testing or // in rare cases where the port isn't 53. func (r *Resolver) newResolverConn( logWarning func(error), serverAddr string) (retConn net.Conn, retErr error) { defer func() { if retErr != nil { logWarning(retErr) } }() // When configured, attempt to synthesize an IPv6 address from // an IPv4 address for compatibility on DNS64/NAT64 networks. // If synthesize fails, try the original address. if r.networkConfig.IPv6Synthesize != nil { serverIPStr, port, err := net.SplitHostPort(serverAddr) if err != nil { return nil, errors.Trace(err) } serverIP := net.ParseIP(serverIPStr) if serverIP != nil && serverIP.To4() != nil { synthesized := r.networkConfig.IPv6Synthesize(serverIPStr) if synthesized != "" && net.ParseIP(synthesized) != nil { serverAddr = net.JoinHostPort(synthesized, port) } } } dialer := &net.Dialer{} if r.networkConfig.BindToDevice != nil { dialer.Control = func(_, _ string, c syscall.RawConn) error { var controlErr error err := c.Control(func(fd uintptr) { _, err := r.networkConfig.BindToDevice(int(fd)) if err != nil { controlErr = errors.Tracef("BindToDevice failed: %v", err) return } }) if controlErr != nil { return errors.Trace(controlErr) } return errors.Trace(err) } } // context.Background is ok in this case as the UDP dial is just a local // syscall to create the socket. conn, err := dialer.DialContext(context.Background(), "udp", serverAddr) if err != nil { return nil, errors.Trace(err) } return conn, nil } func (r *Resolver) updateMetricResolves() { r.mutex.Lock() defer r.mutex.Unlock() r.metrics.resolves += 1 } func (r *Resolver) updateMetricRequestsIPv4() { r.mutex.Lock() defer r.mutex.Unlock() r.metrics.requestsIPv4 += 1 } func (r *Resolver) updateMetricRequestsIPv6() { r.mutex.Lock() defer r.mutex.Unlock() r.metrics.requestsIPv6 += 1 } func (r *Resolver) updateMetricResponsesIPv4() { r.mutex.Lock() defer r.mutex.Unlock() r.metrics.responsesIPv4 += 1 } func (r *Resolver) updateMetricResponsesIPv6() { r.mutex.Lock() defer r.mutex.Unlock() r.metrics.responsesIPv6 += 1 } func (r *Resolver) updateMetricDefaultResolver(success bool) { r.mutex.Lock() defer r.mutex.Unlock() r.metrics.defaultResolves += 1 if success { r.metrics.defaultSuccesses += 1 } } func (r *Resolver) updateMetricPeakInFlight(inFlight int64) { r.mutex.Lock() defer r.mutex.Unlock() if inFlight > r.metrics.peakInFlight { r.metrics.peakInFlight = inFlight } } func (r *Resolver) updateMetricRTT(rtt time.Duration) { r.mutex.Lock() defer r.mutex.Unlock() if rtt < 0 { // Ignore invalid input. return } // When r.metrics.minRTT < 0, min/maxRTT is unset. if r.metrics.minRTT < 0 || rtt < r.metrics.minRTT { r.metrics.minRTT = rtt } if rtt > r.metrics.maxRTT { r.metrics.maxRTT = rtt } } func hasRoutableIPv6Interface() (bool, error) { interfaces, err := net.Interfaces() if err != nil { return false, errors.Trace(err) } for _, in := range interfaces { if (in.Flags&net.FlagUp == 0) || (in.Flags&(net.FlagLoopback|net.FlagPointToPoint)) != 0 { continue } addrs, err := in.Addrs() if err != nil { return false, errors.Trace(err) } for _, addr := range addrs { if IPNet, ok := addr.(*net.IPNet); ok && IPNet.IP.To4() == nil && !IPNet.IP.IsLinkLocalUnicast() { return true, nil } } } return false, nil } func generateIPAddressFromCIDR(CIDR string) (net.IP, error) { _, IPNet, err := net.ParseCIDR(CIDR) if err != nil { return nil, errors.Trace(err) } // A retry is required, since a CIDR may include broadcast IPs (a.b.c.0) or // other invalid values. The number of retries is limited to ensure we // don't hang in the case of a misconfiguration. for i := 0; i < 10; i++ { randBytes := prng.Bytes(len(IPNet.IP)) IP := make(net.IP, len(IPNet.IP)) // The 1 bits in the mask must apply to the IP in the CIDR and the 0 // bits in the mask are available to randomize. for i := 0; i < len(IP); i++ { IP[i] = (IPNet.IP[i] & IPNet.Mask[i]) | (randBytes[i] & ^IPNet.Mask[i]) } if IP.IsGlobalUnicast() && !common.IsBogon(IP) { return IP, nil } } return nil, errors.TraceNew("failed to generate random IP") } type resolverQuestionType int const ( resolverQuestionTypeA = 0 resolverQuestionTypeAAAA = 1 ) func performDNSQuery( resolveCtx context.Context, logWarning func(error), params *ResolveParameters, useProtocolTransform bool, conn net.Conn, questionType resolverQuestionType, hostname string) ([]net.IP, []time.Duration, time.Duration, error) { if useProtocolTransform { if params.ProtocolTransformSpec == nil || params.ProtocolTransformSeed == nil { return nil, nil, -1, errors.TraceNew("invalid protocol transform configuration") } // miekg/dns expects conn to be a net.PacketConn or else it writes the // TCP length prefix udpConn, ok := conn.(*net.UDPConn) if !ok { return nil, nil, -1, errors.TraceNew("conn is not a *net.UDPConn") } conn = &transformDNSPacketConn{ UDPConn: udpConn, transform: params.ProtocolTransformSpec, seed: params.ProtocolTransformSeed, } } // UDPSize sets the receive buffer to > 512, even when we don't include // EDNS(0), which will mitigate issues with RFC 1035 non-compliant // servers. See Go issue 51127. dnsConn := &dns.Conn{ Conn: conn, UDPSize: udpPacketBufferSize, } defer dnsConn.Close() // SetQuestion initializes request.MsgHdr.Id to a random value request := &dns.Msg{MsgHdr: dns.MsgHdr{RecursionDesired: true}} switch questionType { case resolverQuestionTypeA: request.SetQuestion(dns.Fqdn(hostname), dns.TypeA) case resolverQuestionTypeAAAA: request.SetQuestion(dns.Fqdn(hostname), dns.TypeAAAA) default: return nil, nil, -1, errors.TraceNew("unknown DNS request question type") } if params.IncludeEDNS0 { // miekg/dns: "RFC 6891, Section 6.1.1 allows the OPT record to appear // anywhere in the additional record section, but it's usually at the // end..." request.SetEdns0(udpPacketBufferSize, false) } startTime := time.Now() // Send the DNS request dnsConn.WriteMsg(request) // Read and process the DNS response var IPs []net.IP var TTLs []time.Duration var lastErr error RTT := time.Duration(-1) for { // Stop when resolveCtx is done; the caller, ResolveIP, will also // close conn, which will interrupt a blocking dnsConn.ReadMsg. if resolveCtx.Err() != nil { // ResolveIP, which calls performDNSQuery, already records the // context error (e.g., context timeout), so instead report // lastErr, when present, as it may contain more useful // information about why a response was rejected. err := lastErr if err == nil { err = errors.Trace(resolveCtx.Err()) } return nil, nil, RTT, err } // Read a response. RTT is the elapsed time between sending the // request and reading the last received response. response, err := dnsConn.ReadMsg() RTT = time.Since(startTime) if err == nil && response.MsgHdr.Id != request.MsgHdr.Id { err = dns.ErrId } if err != nil { // Try reading again, in case the first response packet failed to // unmarshal or had an invalid ID. The Go resolver also does this; // see Go issue 13281. if resolveCtx.Err() == nil { // Only log if resolveCtx is not done; otherwise the error could // be due to conn being closed by ResolveIP. lastErr = errors.Tracef("invalid response: %v", err) logWarning(lastErr) } continue } // Check the RCode. // // For IPv4, we expect RCodeSuccess as Psiphon will typically only // resolve domains that exist and have a valid IP (when this isn't // the case, and we retry, the overall ResolveIP and its parent dial // will still abort after resolveCtx is done, or RequestTimeout // expires for maxAttempts). // // For IPv6, we should also expect RCodeSuccess even if there is no // AAAA record, as long as the domain exists and has an A record. // However, per RFC 6147 section 5.1.2, we may receive // NXDOMAIN: "...some servers respond with RCODE=3 to a AAAA query // even if there is an A record available for that owner name. Those // servers are in clear violation of the meaning of RCODE 3...". In // this case, we coalesce NXDOMAIN into success to treat the response // the same as success with no AAAA record. // // All other RCodes, which are unexpected, lead to a read retry. if response.MsgHdr.Rcode != dns.RcodeSuccess && !(questionType == resolverQuestionTypeAAAA && response.MsgHdr.Rcode == dns.RcodeNameError) { errMsg, ok := dns.RcodeToString[response.MsgHdr.Rcode] if !ok { errMsg = fmt.Sprintf("Rcode: %d", response.MsgHdr.Rcode) } lastErr = errors.Tracef("unexpected RCode: %v", errMsg) logWarning(lastErr) continue } // Extract all IP answers, along with corresponding TTLs for caching. // Perform additional validation, which may lead to another read // retry. However, if _any_ valid IP is found, stop reading and // return that result. Again, the validation is only best effort. checkFailed := false for _, answer := range response.Answer { haveAnswer := false var IP net.IP var TTLSec uint32 switch questionType { case resolverQuestionTypeA: if a, ok := answer.(*dns.A); ok { IP = a.A TTLSec = a.Hdr.Ttl haveAnswer = true } case resolverQuestionTypeAAAA: if aaaa, ok := answer.(*dns.AAAA); ok { IP = aaaa.AAAA TTLSec = aaaa.Hdr.Ttl haveAnswer = true } } if !haveAnswer { continue } err := checkDNSAnswerIP(IP) if err != nil { checkFailed = true lastErr = errors.Tracef("invalid IP: %v", err) logWarning(lastErr) // Check the next answer continue } IPs = append(IPs, IP) TTLs = append(TTLs, time.Duration(TTLSec)*time.Second) } // For IPv4, an IP is expected, as noted in the comment above. // // In potential cases where we resolve a domain that has only an IPv6 // address, the concurrent AAAA request will deliver its result to // ResolveIP, and that answer will be selected, so only the "await" // logic will delay the parent dial in that case. if questionType == resolverQuestionTypeA && len(IPs) == 0 && !checkFailed { checkFailed = true lastErr = errors.TraceNew("unexpected empty A response") logWarning(lastErr) } // Retry if there are no valid IPs and any error; if no error, this // may be a valid AAAA response with no IPs, in which case return the // result. if len(IPs) == 0 && checkFailed { continue } return IPs, TTLs, RTT, nil } } func checkDNSAnswerIP(IP net.IP) error { if IP == nil { return errors.TraceNew("IP is nil") } // Limitation: this could still be a phony/injected response, it's not // possible to verify with plaintext DNS, but a "bogon" IP is clearly // invalid. if common.IsBogon(IP) { return errors.TraceNew("IP is bogon") } // Create a temporary socket bound to the destination IP. This checks // thats the local host has a route to this IP. If not, we'll reject the // IP. This prevents selecting an IP which is guaranteed to fail to dial. // Use UDP as this results in no network traffic; the destination port is // arbitrary. The Go resolver performs a similar operation. // // Limitations: // - We may cache the IP and reuse it without checking routability again; // the cache should be flushed when network state changes. // - Given that the AAAA is requested only when the host has an IPv6 // route, we don't expect this to often fail with a _valid_ response. // However, this remains a possibility and in this case, // performDNSQuery will keep awaiting a response which can trigger // the "await" logic. conn, err := net.DialUDP("udp", nil, &net.UDPAddr{IP: IP, Port: 443}) if err != nil { return errors.Trace(err) } conn.Close() return nil } func defaultResolverLookupIP( ctx context.Context, hostname string, logHostnames bool) ([]net.IP, error) { addrs, err := net.DefaultResolver.LookupIPAddr(ctx, hostname) if err != nil && !logHostnames { // Remove domain names from "net" error messages. err = common.RedactNetError(err) } if err != nil { return nil, errors.Trace(err) } ips := make([]net.IP, len(addrs)) for i, addr := range addrs { ips[i] = addr.IP } return ips, nil } // transformDNSPacketConn wraps a *net.UDPConn, intercepting Write calls and // applying the specified protocol transform. // // As transforms operate on strings and DNS requests are binary, the transform // should be expressed using hex characters. The DNS packet to be written // (input the Write) is converted to hex, transformed, and converted back to // binary and then actually written to the UDP socket. type transformDNSPacketConn struct { *net.UDPConn transform transforms.Spec seed *prng.Seed } func (conn *transformDNSPacketConn) Write(b []byte) (int, error) { // Limitation: there is no check that a transformed packet remains within // the network packet MTU. input := hex.EncodeToString(b) output, err := conn.transform.Apply(conn.seed, input) if err != nil { return 0, errors.Trace(err) } packet, err := hex.DecodeString(output) if err != nil { return 0, errors.Trace(err) } _, err = conn.UDPConn.Write(packet) if err != nil { // In the error case, don't report bytes written as the number could // exceed the pre-transform length. return 0, errors.Trace(err) } // Report the pre-transform length as bytes written, as the caller may check // that the requested len(b) bytes were written. return len(b), nil }