344 lines
12 KiB
Go
344 lines
12 KiB
Go
package nameserver
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/miekg/dns"
|
|
. "github.com/weaveworks/weave/common"
|
|
)
|
|
|
|
type uniqZoneRecordKey struct {
|
|
name string
|
|
ipv4 IPv4
|
|
}
|
|
|
|
// A group of ZoneRecords where there are no duplicates (according to the name & IPv4)
|
|
type uniqZoneRecords map[uniqZoneRecordKey]ZoneRecord
|
|
|
|
func newUniqZoneRecords() uniqZoneRecords {
|
|
return make(uniqZoneRecords, 0)
|
|
}
|
|
|
|
// Add a new ZoneRecord to the group
|
|
func (uzr *uniqZoneRecords) add(zr ZoneRecord) {
|
|
key := uniqZoneRecordKey{zr.Name(), ipToIPv4(zr.IP())}
|
|
(*uzr)[key] = zr
|
|
}
|
|
|
|
// Return the group as an slice
|
|
func (uzr *uniqZoneRecords) toSlice() []ZoneRecord {
|
|
res := make([]ZoneRecord, len(*uzr))
|
|
i := 0
|
|
for _, r := range *uzr {
|
|
res[i] = r
|
|
i++
|
|
}
|
|
return res
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Lookup in the database for locally-introduced information
|
|
func (zone *ZoneDb) lookup(target string, lfun func(ns *namesSet) []*recordEntry) (res []ZoneRecord, err error) {
|
|
uniq := newUniqZoneRecords()
|
|
for identName, nameset := range zone.idents {
|
|
if identName != defaultRemoteIdent {
|
|
for _, ze := range lfun(nameset) {
|
|
uniq.add(ze)
|
|
}
|
|
}
|
|
}
|
|
if len(uniq) == 0 {
|
|
return nil, LookupError(target)
|
|
}
|
|
return uniq.toSlice(), nil
|
|
}
|
|
|
|
// Perform a lookup for a name in the zone
|
|
// The name can be resolved locally with the local database
|
|
func (zone *ZoneDb) LookupName(name string) (res []ZoneRecord, err error) {
|
|
zone.mx.RLock()
|
|
defer zone.mx.RUnlock()
|
|
|
|
// note: LookupName() is usually called from the mDNS server, so we do not touch the name
|
|
name = dns.Fqdn(name)
|
|
Debug.Printf("[zonedb] Looking for name '%s' in local database", name)
|
|
return zone.lookup(name, func(ns *namesSet) []*recordEntry { return ns.getEntriesForName(name) })
|
|
}
|
|
|
|
// Perform a lookup for a IP address in the zone
|
|
// The address can be resolved locally with the local database
|
|
func (zone *ZoneDb) LookupInaddr(inaddr string) (res []ZoneRecord, err error) {
|
|
zone.mx.RLock()
|
|
defer zone.mx.RUnlock()
|
|
|
|
// note: LookupInaddr() is usually called from the mDNS server, so we do not touch the name
|
|
revIPv4, err := raddrToIPv4(inaddr)
|
|
if err != nil {
|
|
return nil, newParseError("lookup address", inaddr)
|
|
}
|
|
Debug.Printf("[zonedb] Looking for address in local database: '%s' (%s)", revIPv4, inaddr)
|
|
return zone.lookup(inaddr, func(ns *namesSet) []*recordEntry { return ns.getEntriesForIP(revIPv4) })
|
|
}
|
|
|
|
// Perform a domain lookup with mDNS
|
|
func (zone *ZoneDb) domainLookup(target string, lfun ZoneLookupFunc) (res []ZoneRecord, err error) {
|
|
// no local results have been obtained in the local database: try with a mDNS query
|
|
Debug.Printf("[zonedb] '%s' not in local database... trying with mDNS", target)
|
|
lanswers, err := lfun(target)
|
|
if err != nil {
|
|
Debug.Printf("[zonedb] mDNS lookup error for '%s': %s", target, err)
|
|
return nil, err
|
|
}
|
|
|
|
// if the request has been successful, save the IP in the local database and return the corresponding ZoneRecord
|
|
// (we do not get the remote ident in the mDNS reply, so we save it in a "remote" ident)
|
|
Debug.Printf("[zonedb] adding '%s' (obtained with mDNS) to '%s'", lanswers, target)
|
|
res = make([]ZoneRecord, len(lanswers))
|
|
zone.mx.Lock()
|
|
now := zone.clock.Now()
|
|
uniq := newUniqZoneRecords()
|
|
remoteIdent := zone.getNamesSet(defaultRemoteIdent)
|
|
for _, answer := range lanswers {
|
|
r, err := remoteIdent.addIPToName(answer, now)
|
|
if err != nil {
|
|
zone.mx.Unlock()
|
|
Warning.Printf("[zonedb] '%s' insertion for %s failed: %s", answer, target, err)
|
|
return nil, err
|
|
}
|
|
uniq.add(r)
|
|
}
|
|
zone.mx.Unlock()
|
|
|
|
return uniq.toSlice(), nil
|
|
}
|
|
|
|
// Perform a lookup for a name in the zone
|
|
// The name can be resolved locally with the local database or with some other resolution method (eg, a mDNS query)
|
|
func (zone *ZoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) {
|
|
name = dns.Fqdn(name)
|
|
Debug.Printf("[zonedb] Looking for name '%s' in local(&remote) database", name)
|
|
|
|
zone.mx.RLock()
|
|
now := zone.clock.Now()
|
|
uniq := newUniqZoneRecords()
|
|
for identName, nameset := range zone.idents {
|
|
for _, ze := range nameset.getEntriesForName(name) {
|
|
// filter the entries with expired TTL
|
|
// locally introduced entries are never expired: they always have TTL=0
|
|
if ze.hasExpired(now) {
|
|
Debug.Printf("[zonedb] '%s': expired entry '%s' ignored: removing", name, ze)
|
|
nameset.deleteName(name)
|
|
} else {
|
|
uniq.add(ze)
|
|
}
|
|
}
|
|
if identName != defaultRemoteIdent {
|
|
nameset.touchName(name, now)
|
|
}
|
|
}
|
|
zone.mx.RUnlock()
|
|
|
|
if len(uniq) > 0 {
|
|
Debug.Printf("[zonedb] '%s' resolved in local database", name)
|
|
res = uniq.toSlice()
|
|
} else {
|
|
res, err = zone.domainLookup(name, zone.mdnsCli.LookupName)
|
|
}
|
|
|
|
if len(res) > 0 {
|
|
zone.startUpdatingName(name)
|
|
return res, nil
|
|
}
|
|
return nil, LookupError(name)
|
|
}
|
|
|
|
// Perform a lookup for a IP address in the zone
|
|
// The address can be resolved either with the local database or
|
|
// with some other resolution method (eg, a mDNS query)
|
|
func (zone *ZoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err error) {
|
|
revIPv4, err := raddrToIPv4(inaddr)
|
|
if err != nil {
|
|
return nil, newParseError("lookup address", inaddr)
|
|
}
|
|
|
|
Debug.Printf("[zonedb] Looking for address in local(&remote) database: '%s' (%s)", revIPv4, inaddr)
|
|
|
|
zone.mx.RLock()
|
|
now := zone.clock.Now()
|
|
uniq := newUniqZoneRecords()
|
|
for identName, nameset := range zone.idents {
|
|
for _, ze := range nameset.getEntriesForIP(revIPv4) {
|
|
// filter the entries with expired TTL
|
|
// locally introduced entries are never expired: they always have TTL=0
|
|
if ze.hasExpired(now) {
|
|
Debug.Printf("[zonedb] '%s': expired entry '%s' ignored: removing", revIPv4, ze)
|
|
nameset.deleteIP(revIPv4)
|
|
} else {
|
|
uniq.add(ze)
|
|
if identName != defaultRemoteIdent {
|
|
nameset.touchName(ze.Name(), now)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
zone.mx.RUnlock()
|
|
|
|
if len(uniq) > 0 {
|
|
Debug.Printf("[zonedb] '%s' resolved in local database", inaddr)
|
|
res = uniq.toSlice()
|
|
} else {
|
|
res, err = zone.domainLookup(inaddr, zone.mdnsCli.LookupInaddr)
|
|
}
|
|
|
|
if len(res) > 0 {
|
|
// note: even for reverse addresses, we perform the background updates in the name, not in the IP
|
|
// this simplifies the process and produces basically the same results...
|
|
// note: we do not spend time trying to update names that did not return an initial response...
|
|
for _, r := range res {
|
|
zone.startUpdatingName(r.Name())
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
return nil, LookupError(inaddr)
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Names updates try to find all the IPs for a given name with a mDNS query
|
|
//
|
|
// There are two types of names updates:
|
|
//
|
|
// - immediate updates.
|
|
// After a `DomainLookup*()` for a name not in the database we will return the
|
|
// first IP we can get with mDNS from other peers. Waiting for more responses would
|
|
// mean more latency in the response to the client, so we send only one answer BUT
|
|
// we also trigger an immediate update request for that name in order to get all
|
|
// the other IPs we didn't wait for...
|
|
//
|
|
// - periodic updates
|
|
// once we have obtained the first group of IPs for a name, we schedule a periodic
|
|
// refresh for that name, so we keep the list of IPs for that name up to date.
|
|
//
|
|
// These names updates are repeated until either
|
|
//
|
|
// a) there is no interest in the name, determined by a global 'relevant time'
|
|
// and the last time some local client asked about the name,
|
|
// or
|
|
// b) no peers answer one of our refresh requests (because the name has probably
|
|
// disappeared from the network)
|
|
//
|
|
|
|
// TODO: for the sake of simplicity, we implement this mechanism with two channels: one for immediate
|
|
// updates and another one for scheduled requests. We could use a heap of "next-time"s and a timer,
|
|
// but that would require timers cancellations/updates/etc on insertions/removals/etc, and
|
|
// it is probably not worth the trouble as we always use the same refresh period.
|
|
// It could be useful if we move to a solution where we set update times from the responses TTLs,
|
|
// but we currently use a fixed TTL (30secs), the same as the refresh period...
|
|
// Anyway, maybe we will move to a gossip-based solution instead of doing this polling...
|
|
|
|
// Check if we must start updating a name and, in that case, trigger a immediate update
|
|
func (zone *ZoneDb) startUpdatingName(name string) {
|
|
if zone.refreshInterval > 0 {
|
|
zone.mx.Lock()
|
|
defer zone.mx.Unlock()
|
|
|
|
// check if we should enqueue a refresh request for this name
|
|
n := zone.getNamesSet(defaultRemoteIdent).getName(name, true)
|
|
if n.lastRefreshTime.IsZero() {
|
|
now := zone.clock.Now()
|
|
n.lastRefreshTime = now
|
|
|
|
Debug.Printf("[zonedb] Creating new immediate refresh request for '%s'", name)
|
|
zone.refreshChan <- refreshRequest{name: name, time: now}
|
|
}
|
|
}
|
|
}
|
|
|
|
// A worker for updating the list of IPs we have for a name
|
|
func (zone *ZoneDb) updater(num int) {
|
|
defer zone.refreshWg.Done()
|
|
|
|
Debug.Printf("[zonedb] Starting background updater #%d...", num)
|
|
for {
|
|
select {
|
|
case <-zone.refreshCloseChan:
|
|
Debug.Printf("[zonedb] Background updater #%d: interrupted while waiting for requests: exiting", num)
|
|
return
|
|
|
|
case request := <-zone.refreshChan:
|
|
// if nobody has asked for this name for long time, just forget about it...
|
|
// this will eventually garbage collect the `refreshChan` and all remote info in absence of activity
|
|
if !zone.IsNameRelevant(request.name) || zone.IsNameExpired(request.name) {
|
|
Debug.Printf("[zonedb] '%s' seem to be irrelevant now: removing any remote information", request.name)
|
|
zone.mx.Lock()
|
|
zone.getNamesSet(defaultRemoteIdent).deleteName(request.name)
|
|
zone.mx.Unlock()
|
|
continue
|
|
}
|
|
|
|
// perform the refresh for this name
|
|
name := dns.Fqdn(request.name)
|
|
Debug.Printf("[zonedb] Refreshing name '%s' with mDNS...", name)
|
|
res, _ := zone.mdnsCli.InsistentLookupName(request.name)
|
|
if res != nil && len(res) > 0 {
|
|
numIps := len(res)
|
|
zone.mx.Lock()
|
|
now := zone.clock.Now()
|
|
added, removed := zone.getNamesSet(defaultRemoteIdent).getName(name, true).updateIPs(res, now)
|
|
zone.mx.Unlock()
|
|
Debug.Printf("[zonedb] Obtained %d IPs for name '%s' with mDNS: %d added, %d removed",
|
|
numIps, name, added, removed)
|
|
|
|
// once the name has been updated, we insert the request (back) in the periodic requests channel
|
|
now = zone.clock.Now()
|
|
request.time = now.Add(zone.refreshInterval)
|
|
Debug.Printf("[zonedb] Rescheduling update for '%s' in %.2f secs",
|
|
request.name, zone.refreshInterval.Seconds())
|
|
zone.refreshSchedChan <- request
|
|
} else {
|
|
Debug.Printf("[zonedb] nobody knows about '%s'... removing", name)
|
|
zone.mx.Lock()
|
|
zone.getNamesSet(defaultRemoteIdent).deleteName(request.name)
|
|
zone.mx.Unlock()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The periodic updater
|
|
// Consume requests from the `refreshSchedChan`, where requests with increasing scheduling time are enqueued
|
|
// for refreshing names...
|
|
func (zone *ZoneDb) periodicUpdater() {
|
|
defer zone.refreshWg.Done()
|
|
for {
|
|
select {
|
|
case <-zone.refreshCloseChan:
|
|
Debug.Printf("[zonedb] Periodic updater: interrupted while waiting for requests: exiting")
|
|
return
|
|
|
|
case request := <-zone.refreshSchedChan:
|
|
now := zone.clock.Now()
|
|
// we can sleep until the update time has arrived, as requests are sorted by scheduled time (new request
|
|
// in this channel will always be scheduled later than the last item in the channel), so we can
|
|
// safely suspend this goroutine until then...
|
|
if request.time.After(now) {
|
|
ddiff := time.Duration(request.time.Sub(now).Nanoseconds())
|
|
timer := zone.clock.Timer(ddiff)
|
|
Debug.Printf("[zonedb] Periodic updater: new request for %s: sleeping for %.2f secs...",
|
|
request.name, ddiff.Seconds())
|
|
select {
|
|
case <-zone.refreshCloseChan:
|
|
Debug.Printf("[zonedb] Periodic updater: interrupted while sleeping: exiting")
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
}
|
|
|
|
// once the time has arrived, we insert the request in the immediate refresh channel
|
|
zone.refreshChan <- request
|
|
}
|
|
}
|
|
}
|