package traverse import ( "fmt" "sort" miekgdns "github.com/miekg/dns" ) // ServerResponse wraps a DecodedQuery for one (server, IP) query, mirroring // response.rb: it owns a child InfoCache seeded with the in-bailiwick records, // upgrades referral → referral_lame, and computes the start servers for // referral/restart children. The noglue/loop variants (response_noglue.rb, // response_loop.rb) are synthetic — no query was sent, DQ is nil. type ServerResponse struct { DQ *DecodedQuery Status Status Qname string Qclass uint16 Qtype uint16 IP string Server string Bailiwick string // ParentIP is the address of the referring server; it is part of the // stats key for referral_lame so lame referrals from different parents // stay separate. ParentIP string Cache *InfoCache Starters []StartServer StartersBailiwick string } // NewServerResponse evaluates a decoded query in the context of parentCache. // server is the NS hostname that was queried (dq.IP is its address). func NewServerResponse(dq *DecodedQuery, server, parentIP string, parentCache *InfoCache) (*ServerResponse, error) { r := &ServerResponse{ DQ: dq, Status: dq.Status, Qname: dq.Qname, Qclass: dq.Qclass, Qtype: dq.Qtype, IP: dq.IP, Server: canonicalName(server), Bailiwick: dq.Bailiwick, ParentIP: parentIP, Cache: NewInfoCache(parentCache), } if err := r.evaluate(); err != nil { return nil, err } return r, nil } // NewNoGlueResponse records a dead end: ip referred us to server inside // bailiwick without glue and there is no way to resolve it. func NewNoGlueResponse(qname string, qclass, qtype uint16, ip, server, bailiwick string) *ServerResponse { return &ServerResponse{ Status: StatusNoGlue, Qname: canonicalName(qname), Qclass: qclass, Qtype: qtype, IP: ip, Server: canonicalName(server), Bailiwick: canonicalName(bailiwick), } } // NewLoopResponse records a dead end: resolving server from ip would repeat // an ancestor referral. func NewLoopResponse(qname string, qclass, qtype uint16, ip, server, bailiwick string) *ServerResponse { return &ServerResponse{ Status: StatusLoop, Qname: canonicalName(qname), Qclass: qclass, Qtype: qtype, IP: ip, Server: canonicalName(server), Bailiwick: canonicalName(bailiwick), } } // evaluate mirrors response.rb#evaluate: cache the in-bailiwick records, then // for referral/restart work out the start servers from THIS branch's cache; // a referral whose new zone is not strictly deeper than the bailiwick is lame. func (r *ServerResponse) evaluate() error { if r.Status != StatusException { r.Cache.Add(r.DQ.CacheableGood) } switch r.DQ.Status { case StatusRestart: starters, bw, err := r.Cache.GetStartServers(r.DQ.Endname) if err != nil { return err } r.Starters, r.StartersBailiwick = starters, bw case StatusReferral: starters, bw, err := r.Cache.GetStartServers(r.DQ.Endname) if err != nil { return err } r.Starters, r.StartersBailiwick = starters, bw if isLameReferral(r.DQ.Bailiwick, bw) { r.Status = StatusReferralLame } starterNames := make([]string, len(starters)) for i, s := range starters { starterNames[i] = s.Name } if !equalSorted(starterNames, r.DQ.AuthorityNames) { r.DQ.WarningsAdd("Referred authority names do not match query cache expectations") } } return nil } func equalSorted(a, b []string) bool { if len(a) != len(b) { return false } as := append([]string(nil), a...) bs := append([]string(nil), b...) sort.Strings(as) sort.Strings(bs) for i := range as { if as[i] != bs[i] { return false } } return true } // StatsKey is the leaf aggregation key (response.rb update_stats_key and the // noglue/loop variants): identical keys merge by summing probability. func (r *ServerResponse) StatsKey() string { qclass := ClassToString(r.Qclass) qtype := TypeToString(r.Qtype) switch r.Status { case StatusNoGlue, StatusLoop: return fmt.Sprintf("key:%s:%s:%s:%s:%s:%s:%s", r.Status, r.IP, r.Qname, qclass, qtype, r.Server, r.Bailiwick) default: key := fmt.Sprintf("key:%s:%s:%s:%s:%s:%s", r.Status, r.IP, r.Server, r.Qname, qclass, qtype) if r.Status == StatusException && r.DQ != nil { key += ":" + r.DQ.ExceptionMessage } else if r.Status == StatusReferralLame { key += ":" + r.ParentIP } return key } } // String renders a short description for progress display (the Ruby // response to_s variants: "No glue for X" / "Loop encountered resolving X"). func (r *ServerResponse) String() string { switch r.Status { case StatusNoGlue: return fmt.Sprintf("No glue for %s", r.Server) case StatusLoop: return fmt.Sprintf("Loop encountered resolving %s", r.Server) case StatusException: if r.DQ != nil { return r.DQ.ExceptionMessage } case StatusError: if r.DQ != nil { return r.DQ.ErrorMessage } } return string(r.Status) } func ClassToString(qclass uint16) string { if s, ok := miekgdns.ClassToString[qclass]; ok { return s } return fmt.Sprintf("CLASS%d", qclass) } func TypeToString(qtype uint16) string { if s, ok := miekgdns.TypeToString[qtype]; ok { return s } return fmt.Sprintf("TYPE%d", qtype) }