From c1d124df4c45b270279c4d5e9cce8414bee10500 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 27 Apr 2015 13:28:47 +0200 Subject: [PATCH 01/21] Interfaces for the mDNS client and server --- nameserver/mdns.go | 28 ++++++++++++++++++++++++++++ nameserver/mdns_server.go | 11 +++++++++-- nameserver/mdns_server_test.go | 2 +- nameserver/server.go | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 nameserver/mdns.go diff --git a/nameserver/mdns.go b/nameserver/mdns.go new file mode 100644 index 00000000..e476fb4f --- /dev/null +++ b/nameserver/mdns.go @@ -0,0 +1,28 @@ +package nameserver + +import "net" + +// A basic mDNS service +type ZoneMDNS interface { + // Start the service + Start(*net.Interface) error + // Stop the service + Stop() error +} + +// A mDNS server +type ZoneMDNSServer interface { + ZoneMDNS + // Return the Zone database used by the server + Zone() Zone +} + +// A mDNS client interface +type ZoneMDNSClient interface { + ZoneMDNS + ZoneLookup + // Perform an insistent lookup for a name + InsistentLookupName(name string) ([]ZoneRecord, error) + // Perform an insistent lookup for a reverse address + InsistentLookupInaddr(inaddr string) ([]ZoneRecord, error) +} diff --git a/nameserver/mdns_server.go b/nameserver/mdns_server.go index 4889b073..d67318c9 100644 --- a/nameserver/mdns_server.go +++ b/nameserver/mdns_server.go @@ -11,13 +11,14 @@ type MDNSServer struct { sendconn *net.UDPConn srv *dns.Server zone Zone + allowLocal bool running bool } // Create a new mDNS server // Nothing will be done (including port bindings) until you `Start()` the server -func NewMDNSServer(zone Zone) (*MDNSServer, error) { - return &MDNSServer{zone: zone}, nil +func NewMDNSServer(zone Zone, local bool) (*MDNSServer, error) { + return &MDNSServer{zone: zone, allowLocal: local}, nil } // Return true if testaddr is a UDP address with IP matching my local i/f @@ -112,6 +113,12 @@ func (s *MDNSServer) makeHandler(qtype uint16, lookup LookupFunc) dns.HandlerFun // assumption that the client wouldn't ask if it already knew // the answer, and if it does ask, it'll be happy to get an // answer. + remoteAddr := rw.RemoteAddr() + if s.addrIsLocal(remoteAddr) && !s.allowLocal { + Debug.Printf("[mdns] srv: mDNS query from local host ('%s'): ignored", remoteAddr) + return + } + // TODO: use any Answer in the query for adding records in the Zone database... if len(r.Answer) == 0 && len(r.Question) > 0 { q := &r.Question[0] diff --git a/nameserver/mdns_server_test.go b/nameserver/mdns_server_test.go index 6a45a511..5b11e60e 100644 --- a/nameserver/mdns_server_test.go +++ b/nameserver/mdns_server_test.go @@ -20,7 +20,7 @@ func TestServerSimpleQuery(t *testing.T) { Info.Println("TestServerSimpleQuery starting") mzone := newMockedZoneWithRecords([]ZoneRecord{testRecord1, testRecord2}) - mdnsServer, err := NewMDNSServer(mzone) + mdnsServer, err := NewMDNSServer(mzone, true) wt.AssertNoErr(t, err) err = mdnsServer.Start(nil) wt.AssertNoErr(t, err) diff --git a/nameserver/server.go b/nameserver/server.go index 3077c8f4..fe607590 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -120,7 +120,7 @@ func NewDNSServer(config DNSServerConfig, zone Zone, iface *net.Interface) (s *D if err != nil { return } - s.mdnsSrv, err = NewMDNSServer(s.Zone) + s.mdnsSrv, err = NewMDNSServer(s.Zone, false) if err != nil { return } From da2d66a6dfe1c7e71c3f1ac6d94136659da1a43e Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 14:08:43 +0200 Subject: [PATCH 02/21] New zone database --- nameserver/addrs.go | 2 +- nameserver/http_test.go | 6 +- nameserver/server.go | 1 - nameserver/server_test.go | 21 +- nameserver/zone.go | 694 ++++++++++++++++++++++++++++++++------ nameserver/zone_lookup.go | 55 +++ nameserver/zone_test.go | 130 +++++-- weavedns/main.go | 30 +- 8 files changed, 791 insertions(+), 148 deletions(-) create mode 100644 nameserver/zone_lookup.go diff --git a/nameserver/addrs.go b/nameserver/addrs.go index 0bdb0dca..1e315a91 100644 --- a/nameserver/addrs.go +++ b/nameserver/addrs.go @@ -25,7 +25,7 @@ type ZoneLookup interface { LookupInaddr(inaddr string) ([]ZoneRecord, error) } -type ZoneObserver interface { +type ZoneObservable interface { // Observe anything that affects a particular name in the zone ObserveName(name string, observer ZoneRecordObserver) error // Observe anything that affects a particular IP in the zone diff --git a/nameserver/http_test.go b/nameserver/http_test.go index 5979d683..8b2b778a 100644 --- a/nameserver/http_test.go +++ b/nameserver/http_test.go @@ -27,7 +27,11 @@ func TestHttp(t *testing.T) { dockerIP = "9.8.7.6" ) - zone := NewZoneDb(DefaultLocalDomain) + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() port := rand.Intn(10000) + 32768 fmt.Println("Http test on port", port) diff --git a/nameserver/server.go b/nameserver/server.go index fe607590..9c066ea6 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -10,7 +10,6 @@ import ( ) const ( - DefaultLocalDomain = "weave.local." // The default name used for the local domain DefaultServerPort = 53 // The default server port DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load DefaultUDPBuflen = 4096 // bigger than the default 512 diff --git a/nameserver/server_test.go b/nameserver/server_test.go index 7e893c5b..2ae5203f 100644 --- a/nameserver/server_test.go +++ b/nameserver/server_test.go @@ -44,8 +44,15 @@ func TestUDPDNSServer(t *testing.T) { ) testCIDR1 := testAddr1 + "/24" - InitDefaultLogging(true) - var zone = NewZoneDb(DefaultLocalDomain) + InitDefaultLogging(testing.Verbose()) + Info.Println("TestUDPDNSServer starting") + + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + ip, _, _ := net.ParseCIDR(testCIDR1) zone.AddRecord(containerID, successTestName, ip) @@ -125,9 +132,15 @@ func TestTCPDNSServer(t *testing.T) { ) dnsAddr := fmt.Sprintf("localhost:%d", testPort) - InitDefaultLogging(true) - var zone = NewZoneDb(DefaultLocalDomain) + InitDefaultLogging(testing.Verbose()) + Info.Println("TestTCPDNSServer starting") + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + // generate a list of `numAnswers` IP addresses var addrs []ZoneRecord bs := make([]byte, 4) diff --git a/nameserver/zone.go b/nameserver/zone.go index ca23dab1..b7360fd8 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -3,41 +3,27 @@ package nameserver import ( "bytes" "fmt" + "github.com/benbjohnson/clock" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" "net" "sync" + "time" ) const ( RDNSDomain = "in-addr.arpa." ) +const ( + DefaultLocalDomain = "weave.local." // The default name used for the local domain + + defaultRemoteIdent = "weave:remote" // Ident used for info obtained from mDNS +) + // +1 to also exclude a dot var rdnsDomainLen = len(RDNSDomain) + 1 -type Zone interface { - AddRecord(ident string, name string, ip net.IP) error - DeleteRecord(ident string, ip net.IP) error - DeleteRecordsFor(ident string) error - Domain() string - ZoneLookup -} - -type dbRecord struct { - Ident string - Name string - IP net.IP -} - -// Very simple data structure for now, with linear searching. -// TODO: make more sophisticated to improve performance. -type ZoneDb struct { - mx sync.RWMutex - recs []dbRecord - domain string -} - type LookupError string func (ops LookupError) Error() string { @@ -51,103 +37,609 @@ func (dup DuplicateError) Error() string { return "Tried to add a duplicate entry" } -func NewZoneDb(domain string) *ZoneDb { - return &ZoneDb{ - domain: domain, +type Zone interface { + ZoneObservable + ZoneLookup + // The domain where we operate (eg, "weave.local.") + Domain() string + // Add a record in the local database + AddRecord(ident string, name string, ip net.IP) error + // Delete a record in the local database + DeleteRecord(ident string, ip net.IP) error + // Delete all records for an ident in the local database + DeleteRecordsFor(ident string) error + // Return a status string + Status() string +} + +/////////////////////////////////////////////////////////////////// +// +// Zone database: +// +// idents names entry +// *-------* *------* *------* +// zone -> | ident | --*--> | name | --*-> | IPv4 | +// *-------* | *------* | *------* +// | ident | *--> | name | *-> | IPv4 | +// *-------* *------* *------* +// ... ... +// +// The zone database keeps an ident per container, but also a special +// ident for names/IPs obtained from remote peers. +// +// Each name can have multiple IPs (so far, we only consider IPv4s), and +// each IP is stored in a `recordEntry` in the database. Entries store +// some additional information like priority and weight of the IP (for +// example, for future use with SRV records) +// + +// An entry in the zone database +type recordEntry struct { + Record + + observers []ZoneRecordObserver // the observers for this record + insTime time.Time // time when this record was inserted +} + +func newRecordEntry(zr ZoneRecord, now time.Time) *recordEntry { + return &recordEntry{ + Record: Record{ + name: zr.Name(), + ip: zr.IP(), + priority: zr.Priority(), + weight: zr.Weight(), + ttl: zr.TTL(), + }, + insTime: now, + observers: make([]ZoneRecordObserver, 0), } } -func (zone *ZoneDb) Domain() string { - return zone.domain +func (i *recordEntry) addIPObserver(zro ZoneRecordObserver) { + i.observers = append(i.observers, zro) } -func (zone *ZoneDb) indexOf(match func(dbRecord) bool) int { - for i, r := range zone.recs { - if match(r) { - return i +// Notify all the IP observers and then remove them all +// IP address observers are notified when +// - the IP is removed +// - the name is removed +func (re *recordEntry) notifyIPObservers() { + numObservers := len(re.observers) + if numObservers > 0 { + Debug.Printf("[zonedb] Notifying %d observers of '%s'", numObservers, re.ip) + for _, observer := range re.observers { + observer() } + re.observers = make([]ZoneRecordObserver, 0) } - return -1 } -func (zone *ZoneDb) String() string { - zone.mx.RLock() - defer zone.mx.RUnlock() +func (re recordEntry) TTL() int { + // if we never set the TTL (eg, when using AddRecord()), return the standard value... + if re.ttl == 0 { + return int(localTTL) + } + return re.ttl +} + +// Check if the info in this record is still valid according to the TTL +func (re recordEntry) hasExpired(now time.Time) bool { + return re.ttl > 0 && now.Sub(re.insTime).Seconds() > float64(re.ttl) +} + +func (re recordEntry) String() string { var buf bytes.Buffer - for _, r := range zone.recs { - fmt.Fprintf(&buf, "%.12s %s %v\n", r.Ident, r.IP, r.Name) + fmt.Fprintf(&buf, "%s", re.Record) + lobs := len(re.observers) + if lobs > 0 { + fmt.Fprintf(&buf, "/OBS:%d", lobs) } return buf.String() } -func (zone *ZoneDb) LookupName(name string) ([]ZoneRecord, error) { +/////////////////////////////////////////////////////////////////// + +// a name, with group of records by IPv4 +type name struct { + name string + ipv4 map[IPv4]*recordEntry // all the IPv4 records for this name + observers []ZoneRecordObserver // the observers for this name + lastAccessTime time.Time // last time a Lookup* used info from this name +} + +func newName(n string) *name { + return &name{ + name: n, + ipv4: make(map[IPv4]*recordEntry), + observers: make([]ZoneRecordObserver, 0), + } +} +func (n name) len() int { return len(n.ipv4) } +func (n name) empty() bool { return n.len() == 0 } +func (n name) hasIP(ip net.IP) bool { return n.hasIPv4(ipToIPv4(ip)) } +func (n name) hasIPv4(ip IPv4) bool { _, found := n.ipv4[ip]; return found } + +// Get all the entries for this name +func (n *name) getAllEntries() (res []*recordEntry) { + for _, entry := range n.ipv4 { + res = append(res, entry) + } + return +} + +// Get the entry for an IP in this name +func (n *name) getEntryForIP(ip IPv4) (res *recordEntry) { + if entry, found := n.ipv4[ip]; found { + res = entry + } + return +} + +// Add a new IPv4 to this name +func (n *name) addIP(zr ZoneRecord, now time.Time) (*recordEntry, error) { + ne := newRecordEntry(zr, now) + ipv4 := ipToIPv4(zr.IP()) + n.ipv4[ipv4] = ne + n.notifyNameObservers() + return ne, nil +} + +// Delete an IP for this name +func (n *name) deleteIP(ip IPv4) bool { + if ipRecord, found := n.ipv4[ip]; found { + ipRecord.notifyIPObservers() + n.notifyNameObservers() + delete(n.ipv4, ip) + return true + } + return false +} + +// Update the list of IPs for this name, adding new ones and removing old IPs... +func (n *name) updateIPs(records []ZoneRecord, now time.Time) (added int, removed int) { + ipsMap := make(map[IPv4]bool) + + // add the new IPs + for _, record := range records { + ipv4 := ipToIPv4(record.IP()) + ipsMap[ipv4] = true + if !n.hasIPv4(ipv4) { + n.addIP(record, now) + added++ + } + } + + // remove the old IPs + for ipv4, ipRecord := range n.ipv4 { + if _, isNew := ipsMap[ipv4]; !isNew { + ipRecord.notifyIPObservers() // we must notify the observers before removing the IP record... + delete(n.ipv4, ipv4) + removed++ + } + } + + // notify observers and update times + if added > 0 || removed > 0 { + n.notifyNameObservers() + } + return +} + +// Add a observer for this name +func (n *name) addNameObserver(observer ZoneRecordObserver) { + n.observers = append(n.observers, observer) +} + +// Notify and flush all the observers +// Name observers are notified when +// - an IP is added +// - an IP is removed +// - the name is removed +func (n *name) notifyNameObservers() { + numObservers := len(n.observers) + if numObservers > 0 { + Debug.Printf("[zonedb] Notifying %d observers of '%s'", numObservers, n.name) + for _, observer := range n.observers { + observer() + } + n.observers = make([]ZoneRecordObserver, 0) + } +} + +// Convert a set of IP records to a comma-separated string +func (n name) String() string { + var buf bytes.Buffer + for _, ip := range n.ipv4 { + fmt.Fprintf(&buf, "%s, ", ip) + } + l := buf.Len() + if l > 2 { + buf.Truncate(l - 2) + } + return buf.String() +} + +/////////////////////////////////////////////////////////////////// + +// all the names in a ident +type namesSet struct { + names map[string]*name +} + +func newNamesSet() *namesSet { return &namesSet{names: make(map[string]*name)} } +func (ns namesSet) len() int { return len(ns.names) } +func (ns namesSet) empty() bool { return ns.len() == 0 } +func (ns namesSet) hasName(name string) bool { + if n, found := ns.names[name]; !found { + return false + } else { + // the name must have some valid IPs in order to be considered as "existent" + return len(n.ipv4) > 0 + } +} + +// Get the zone entries for a name +// If the name does not exist and `create` is true, a new name will be created +func (ns *namesSet) getName(name string, create bool) (n *name) { + if n, found := ns.names[name]; !found && create { + newName := newName(name) + ns.names[name] = newName + return newName + } else { + return n + } +} + +// Get all the entries for a name +func (ns *namesSet) getEntriesForName(name string) (res []*recordEntry) { + n := ns.getName(name, false) + if n != nil { + res = n.getAllEntries() + } + return +} + +// Get all the entries for an IP, for all names +func (ns *namesSet) getEntriesForIP(ip IPv4) (res []*recordEntry) { + for _, name := range ns.names { + entry := name.getEntryForIP(ip) + if entry != nil { + res = append(res, entry) + } + } + return +} + +// Add a new IPv4 to a name +func (ns *namesSet) addIPToName(zr ZoneRecord, now time.Time) (*recordEntry, error) { + n := ns.getName(zr.Name(), true) + ipv4 := ipToIPv4(zr.IP()) + if n.hasIPv4(ipv4) { + return n.getEntryForIP(ipv4), DuplicateError{} + } + ns.touchName(zr.Name(), now) + return n.addIP(zr, now) +} + +// Delete a name in the names set +func (ns *namesSet) deleteName(n string) error { + if name, found := ns.names[n]; found { + for _, ipRecord := range name.getAllEntries() { + ipRecord.notifyIPObservers() + } + name.notifyNameObservers() + delete(ns.names, n) + return nil + } else { + return LookupError(fmt.Sprintf("%+v", name)) + } +} + +// Delete an IPv4 from all names +func (ns *namesSet) deleteIP(ip IPv4) error { + cnt := 0 + for nkey, n := range ns.names { + if n.deleteIP(ip) { + cnt++ + } + if n.empty() { + delete(ns.names, nkey) + } + } + + // we must return an error if no records have been found and deleted + if cnt == 0 { + return LookupError(fmt.Sprintf("%+v", ip)) + } + return nil +} + +// Get the name query time +func (ns namesSet) getNameLastAccess(n string) time.Time { + if name, found := ns.names[n]; found { + return name.lastAccessTime + } + return time.Time{} +} + +// Touch the last access time for a name +// The access time is saved only for locally-introduced records (otherwise it could +// be lost when names are irrelevant...) +func (ns *namesSet) touchName(name string, now time.Time) { + Debug.Printf("[zonedb] Touching name %s", name) + n := ns.getName(name, false) + if n != nil { + n.lastAccessTime = now + } +} + +func (ns namesSet) String() string { + var buf bytes.Buffer + for _, name := range ns.names { + fmt.Fprintf(&buf, "%s, ", name) + } + l := buf.Len() + if l > 2 { + buf.Truncate(l - 2) + } + return buf.String() +} + +/////////////////////////////////////////////////////////////////// + +// Names sets, by ident name +type identRecordsSet map[string]*namesSet + +func (irs identRecordsSet) String() string { + var buf bytes.Buffer + for ident, nameset := range irs { + fmt.Fprintf(&buf, "%.12s: %s\n", ident, nameset) + } + return buf.String() +} + +/////////////////////////////////////////////////////////////////// + +type ZoneConfig struct { + // The local domain + Domain string + // The interface where mDNS operates + Iface *net.Interface + // Force a specific mDNS client + MDNSClient ZoneMDNSClient + // Force a specific mDNS server + MDNSServer ZoneMDNSServer + // For a specific clock provider + Clock clock.Clock +} + +// Zone database +// The zone database contains the locally known information about the zone. +// The zone database uses some other mechanisms (eg, mDNS) for finding unknown +// information or for keeping current information up to date. +type zoneDb struct { + mx sync.RWMutex + idents identRecordsSet + + mdnsCli ZoneMDNSClient + mdnsSrv ZoneMDNSServer + iface *net.Interface + domain string // the local domain + clock clock.Clock +} + +// Create a new zone database +func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { + zone = &zoneDb{ + domain: config.Domain, + idents: make(identRecordsSet), + mdnsCli: config.MDNSClient, + mdnsSrv: config.MDNSServer, + iface: config.Iface, + clock: config.Clock, + } + + // fix the default configuration parameters + if zone.clock == nil { + zone.clock = clock.New() + } + if len(zone.domain) == 0 { + zone.domain = DefaultLocalDomain + } + + // Create the mDNS client and server + if zone.mdnsCli == nil { + if zone.mdnsCli, err = NewMDNSClient(); err != nil { + return + } + } + if zone.mdnsSrv == nil { + if zone.mdnsSrv, err = NewMDNSServer(zone, false); err != nil { + return + } + } + + return +} + +// Start the zone database +func (zone *zoneDb) Start() (err error) { + if zone.iface != nil { + Info.Printf("[zonedb] Using mDNS on %s", zone.iface) + } else { + Info.Printf("[zonedb] Using mDNS on all interfaces") + } + + if err = zone.mdnsCli.Start(zone.iface); err != nil { + return + } + if err = zone.mdnsSrv.Start(zone.iface); err != nil { + return + } + return +} + +// Perform a graceful shutdown of the zone database +func (zone *zoneDb) Stop() error { + Debug.Printf("[zonedb] Exiting mDNS client and server...") + zone.mdnsCli.Stop() + zone.mdnsSrv.Stop() + return nil +} + +// Obtain the domain where this database keeps information for +func (zone *zoneDb) Domain() string { + return zone.domain +} + +// Return the status string +func (zone *zoneDb) Status() string { + var buf bytes.Buffer + fmt.Fprintln(&buf, "Local domain", zone.domain) + fmt.Fprintln(&buf, "Interface", zone.iface) + fmt.Fprintf(&buf, "Zone database:\n%s", zone) + return buf.String() +} + +// Add a record with `name` pointing to `ip` +func (zone *zoneDb) AddRecord(ident string, name string, ip net.IP) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + Debug.Printf("[zonedb] Adding record: '%s'/'%s'[%s]", ident, name, ip) + record := Record{dns.Fqdn(name), ip, 0, 0, 0} + _, err = zone.getNamesSet(ident).addIPToName(record, zone.clock.Now()) + return +} + +// Delete all records for an ident +func (zone *zoneDb) DeleteRecordsFor(ident string) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + Debug.Printf("[zonedb] Deleting records for ident '%s'", ident) + zone.deleteIdent(ident) + return +} + +// Delete an IP in a ident +func (zone *zoneDb) DeleteRecord(ident string, ip net.IP) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + + Debug.Printf("[zonedb] Deleting record '%s'/[%s]", ident, ip) + if _, found := zone.idents[ident]; found { + err = zone.idents[ident].deleteIP(ipToIPv4(ip)) + if zone.idents[ident].empty() { + zone.deleteIdent(ident) + } + } else { + err = LookupError(ident) + } + return +} + +// Observe a name. +// The name must have at least one valid IP address. +// Name observers are notified when +// - an IP is added +// - an IP is removed +// - the name is removed +// The observer will be invoked on any change/removal that affects this name. +// Each observer will be invoked at least once (but possibly more). After that, they will be removed. +// The observer should not try to lock the ZoneDB (you will get a deadlock) +func (zone *zoneDb) ObserveName(name string, observer ZoneRecordObserver) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + + cnt := 0 + name = dns.Fqdn(name) + for _, nameset := range zone.idents { + if n := nameset.getName(name, false); n != nil { + n.addNameObserver(observer) + cnt += 1 + } + } + if cnt == 0 { + err = LookupError(fmt.Sprintf("%s", name)) + } + return +} + +// Observe a IP address. +// The IP address must be exit in the database. +// IP address observers are notified when +// - the IP is removed +// - the name is removed +// The observer will be invoked on any change/removal that affects this IP. +// Each observer will be invoked at least once (but possibly more). After that, they will be removed. +// The observer should not try to lock the ZoneDB (you will get a deadlock) +func (zone *zoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (err error) { + zone.mx.Lock() + defer zone.mx.Unlock() + + cnt := 0 + revIP, err := raddrToIPv4(inaddr) + if err != nil { + return newParseError("lookup address", inaddr) + } + for _, nameset := range zone.idents { + for _, entry := range nameset.getEntriesForIP(revIP) { + entry.addIPObserver(observer) + cnt += 1 + } + } + if cnt == 0 { + err = LookupError(fmt.Sprintf("%+v/%+v", inaddr, revIP)) + } + return +} + +// Get the string representation of a zone +func (zone zoneDb) String() string { zone.mx.RLock() defer zone.mx.RUnlock() - for _, r := range zone.recs { - if r.Name == name { - return []ZoneRecord{Record{r.Name, r.IP, 0, 0, 0}}, nil - } - } - return nil, LookupError(name) + return zone.idents.String() } -func (zone *ZoneDb) LookupInaddr(inaddr string) ([]ZoneRecord, error) { - if revIP := net.ParseIP(inaddr[:len(inaddr)-rdnsDomainLen]); revIP != nil { - revIP4 := revIP.To4() - ip := []byte{revIP4[3], revIP4[2], revIP4[1], revIP4[0]} - Debug.Printf("[zonedb] Looking for address: %+v", ip) - zone.mx.RLock() - defer zone.mx.RUnlock() - for _, r := range zone.recs { - if r.IP.Equal(ip) { - return []ZoneRecord{Record{r.Name, r.IP, 0, 0, 0}}, nil - } - } - return nil, LookupError(inaddr) - } - Warning.Printf("[zonedb] Asked to reverse lookup %s", inaddr) - return nil, LookupError(inaddr) -} - -func (zone *ZoneDb) AddRecord(ident string, name string, ip net.IP) error { - zone.mx.Lock() - defer zone.mx.Unlock() - fqdn := dns.Fqdn(name) - pred := func(r dbRecord) bool { return r.Name == fqdn && r.IP.Equal(ip) && r.Ident == ident } - if index := zone.indexOf(pred); index != -1 { - return DuplicateError{} - } - zone.recs = append(zone.recs, dbRecord{ident, fqdn, ip}) - return nil -} - -func (zone *ZoneDb) DeleteRecord(ident string, ip net.IP) error { - zone.mx.Lock() - defer zone.mx.Unlock() - pred := func(r dbRecord) bool { return r.Ident == ident && r.IP.Equal(ip) } - if index := zone.indexOf(pred); index != -1 { - zone.recs = append(zone.recs[:index], zone.recs[index+1:]...) - return nil - } - return LookupError(ident) -} - -func (zone *ZoneDb) DeleteRecordsFor(ident string) error { - zone.mx.Lock() - defer zone.mx.Unlock() - w := 0 // write index - - for _, r := range zone.recs { - if r.Ident != ident { - zone.recs[w] = r - w++ - } - } - zone.recs = zone.recs[:w] - return nil -} - -func (zone *ZoneDb) ContainerDied(ident string) error { +// Notify that a container has died +func (zone *zoneDb) ContainerDied(ident string) error { Info.Printf("[zonedb] Container %s down. Removing records", ident) return zone.DeleteRecordsFor(ident) } + +// Return true if the we have obtained some information for a name from remote peers +// We can have both local (eg, introduced through the HTTP API) and remote (eg, mDNS) +// information for a name. This method only checks if there is remote information... +func (zone *zoneDb) HasNameRemoteInfo(n string) bool { + zone.mx.Lock() + defer zone.mx.Unlock() + + remoteNamesSet := zone.getNamesSet(defaultRemoteIdent) + if remoteNamesSet != nil { + return remoteNamesSet.hasName(n) + } + return false +} + +// Get or create a names set +func (zone *zoneDb) getNamesSet(ident string) *namesSet { + ns, found := zone.idents[ident] + if !found { + ns = newNamesSet() + zone.idents[ident] = ns + } + return ns +} + +// Delete a ident +func (zone *zoneDb) deleteIdent(ident string) { + Info.Printf("[zonedb] Removing everything for ident '%s'", ident) + if ns, found := zone.idents[ident]; found { + for _, n := range ns.names { + for _, entry := range n.getAllEntries() { + entry.notifyIPObservers() + } + n.notifyNameObservers() + } + delete(zone.idents, ident) + } +} diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go new file mode 100644 index 00000000..985f84ae --- /dev/null +++ b/nameserver/zone_lookup.go @@ -0,0 +1,55 @@ +package nameserver + +import ( + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" +) + +// 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) + for identName, nameset := range zone.idents { + if identName != defaultRemoteIdent { + for _, ze := range nameset.getEntriesForName(name) { + res = append(res, ze) + } + } + } + + if len(res) == 0 { + err = LookupError(name) + } + return +} + +// 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) + for identName, nameset := range zone.idents { + if identName != defaultRemoteIdent { + for _, ze := range nameset.getEntriesForIP(revIPv4) { + res = append(res, ZoneRecord(ze)) + } + } + } + if len(res) == 0 { + err = LookupError(inaddr) + } + return +} diff --git a/nameserver/zone_test.go b/nameserver/zone_test.go index a807464f..968b4b7d 100644 --- a/nameserver/zone_test.go +++ b/nameserver/zone_test.go @@ -1,6 +1,8 @@ package nameserver import ( + "fmt" + . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" "net" "testing" @@ -8,60 +10,118 @@ import ( func TestZone(t *testing.T) { var ( - containerID = "deadbeef" - otherContainerID = "cowjuice" - successTestName = "test1.weave." - testAddr1 = "10.2.2.1/24" + container1 = "deadbeef" + container2 = "cowjuice" + name1 = "test1.weave." + name1Addr1 = "10.9.2.1/24" + name1Addr2 = "10.9.2.2/24" + revName1Addr1 = "1.2.9.10.in-addr.arpa." ) - var zone = NewZoneDb(DefaultLocalDomain) + InitDefaultLogging(testing.Verbose()) - ip, _, _ := net.ParseCIDR(testAddr1) - err := zone.AddRecord(containerID, successTestName, ip) + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + + ip1, _, _ := net.ParseCIDR(name1Addr1) + t.Logf("Adding '%s'/%s to '%s'", name1, ip1, container1) + err = zone.AddRecord(container1, name1, ip1) wt.AssertNoErr(t, err) // Add a few more records to make the job harder - err = zone.AddRecord("abcdef0123", "adummy.weave.", net.ParseIP("10.2.0.1")) + t.Logf("Adding 'adummy.weave.' to 'abcdef0123'") + err = zone.AddRecord("abcdef0123", "adummy.weave.", net.ParseIP("10.9.0.1")) wt.AssertNoErr(t, err) - err = zone.AddRecord("0123abcdef", "zdummy.weave.", net.ParseIP("10.2.0.2")) + t.Logf("Adding 'zdummy.weave.' to '0123abcdef'") + err = zone.AddRecord("0123abcdef", "zdummy.weave.", net.ParseIP("10.9.0.2")) + wt.AssertNoErr(t, err) + t.Logf("Zone database:\n%s", zone) + + t.Logf("Checking if we can find the name '%s'", name1) + foundIPs, err := zone.LookupName(name1) wt.AssertNoErr(t, err) - // Check that the address is now there. - foundIP, err := zone.LookupName(successTestName) - wt.AssertNoErr(t, err) - - if !foundIP[0].IP().Equal(ip) { - t.Fatal("Unexpected result for", successTestName, foundIP) + if !foundIPs[0].IP().Equal(ip1) { + t.Fatal("Unexpected result for", name1, foundIPs) } - // See if we can find the address by IP. - foundName, err := zone.LookupInaddr("1.2.2.10.in-addr.arpa.") + t.Logf("Checking if we cannot find some silly name like 'something.wrong'") + foundIPs, err = zone.LookupName("something.wrong") + wt.AssertErrorType(t, err, (*LookupError)(nil), fmt.Sprintf("unknown name: %+v", foundIPs)) + + ip2, _, _ := net.ParseCIDR(name1Addr2) + t.Logf("Adding a second IP for '%s'/%s to '%s'", name1, ip2, container1) + err = zone.AddRecord(container1, name1, ip2) wt.AssertNoErr(t, err) - if foundName[0].Name() != successTestName { - t.Fatal("Unexpected result for", ip, foundName) + t.Logf("Checking if we can find both the old IP and the new IP for '%s'", name1) + foundIPs, err = zone.LookupName(name1) + wt.AssertNoErr(t, err) + if !(foundIPs[0].IP().Equal(ip1) || foundIPs[1].IP().Equal(ip1)) { + t.Fatal("Unexpected result for", name1, foundIPs) + } + if !(foundIPs[0].IP().Equal(ip2) || foundIPs[1].IP().Equal(ip2)) { + t.Fatal("Unexpected result for", name1, foundIPs) } - err = zone.AddRecord(containerID, successTestName, ip) + t.Logf("Checking if we can find the address by IP '1.2.9.10.in-addr.arpa.'") + foundNames, err := zone.LookupInaddr("1.2.9.10.in-addr.arpa.") + wt.AssertNoErr(t, err) + + if foundNames[0].Name() != name1 { + t.Fatal("Unexpected result for", ip1, foundNames) + } + + t.Logf("Checking we can not find an unknown address '30.20.10.1.in-addr.arpa.'") + foundNames, err = zone.LookupInaddr("30.20.10.1.in-addr.arpa.") + wt.AssertErrorType(t, err, (*LookupError)(nil), fmt.Sprintf("unknown IP: %+v", foundNames)) + + t.Logf("Checking if adding again '%s'/%s in %s results in an error", name1, ip1, container1) + err = zone.AddRecord(container1, name1, ip1) wt.AssertErrorType(t, err, (*DuplicateError)(nil), "duplicate add") - err = zone.AddRecord(otherContainerID, successTestName, ip) - // Delete the record for the original container - err = zone.DeleteRecord(containerID, ip) + t.Logf("Adding '%s'/%s in %s too", name1, ip1, container2) + err = zone.AddRecord(container2, name1, ip1) wt.AssertNoErr(t, err) - _, err = zone.LookupName(successTestName) + name1Removed := 0 + err = zone.ObserveName(name1, func() { t.Logf("Observer #1 for '%s' notified.", name1); name1Removed++ }) + wt.AssertNoErr(t, err) + err = zone.ObserveName(name1, func() { t.Logf("Observer #2 for '%s' notified.", name1); name1Removed++ }) + wt.AssertNoErr(t, err) + err = zone.ObserveInaddr(revName1Addr1, func() { t.Logf("Observer #1 for '%s' notified.", revName1Addr1); name1Removed++ }) wt.AssertNoErr(t, err) - err = zone.DeleteRecord(otherContainerID, ip) + t.Logf("Zone database:\n%s", zone) + t.Logf("Deleting the %s in %s", ip1, container1) + err = zone.DeleteRecord(container1, ip1) + wt.AssertNoErr(t, err) + t.Logf("Zone database:\n%s", zone) + + t.Logf("Checking %s's observers have been notified on removal", name1) + if name1Removed < 3 { + t.Logf("Zone database:\n%s", zone) + t.Fatalf("Unexpected number (%d) of calls to observers", name1Removed) + } + + t.Logf("Checking %s can be found", name1) + _, err = zone.LookupName(name1) wt.AssertNoErr(t, err) - // Check that the address is not there now. - _, err = zone.LookupName(successTestName) + t.Logf("Checking %s is not found after removing %s it in %s and %s in %s", + name1, ip1, container2, ip2, container1) + err = zone.DeleteRecord(container1, ip2) + err = zone.DeleteRecord(container2, ip1) + wt.AssertNoErr(t, err) + t.Logf("Zone database:\n%s", zone) + _, err = zone.LookupName(name1) wt.AssertErrorType(t, err, (*LookupError)(nil), "after deleting record") - // Delete a record that isn't there - err = zone.DeleteRecord(containerID, net.ParseIP("0.0.0.0")) + t.Logf("Checking if removing an unknown record results in an error") + err = zone.DeleteRecord(container1, net.ParseIP("0.0.0.0")) wt.AssertErrorType(t, err, (*LookupError)(nil), "when deleting record that doesn't exist") } @@ -72,14 +132,22 @@ func TestDeleteFor(t *testing.T) { addr1 = "10.2.2.3/24" addr2 = "10.2.7.8/24" ) - zone := NewZoneDb(DefaultLocalDomain) + + InitDefaultLogging(testing.Verbose()) + + zone, err := NewZoneDb(ZoneConfig{}) + wt.AssertNoErr(t, err) + err = zone.Start() + wt.AssertNoErr(t, err) + defer zone.Stop() + for _, addr := range []string{addr1, addr2} { ip, _, _ := net.ParseCIDR(addr) err := zone.AddRecord(id, name, ip) wt.AssertNoErr(t, err) } - _, err := zone.LookupName(name) + _, err = zone.LookupName(name) wt.AssertNoErr(t, err) err = zone.DeleteRecordsFor(id) diff --git a/weavedns/main.go b/weavedns/main.go index 6a888896..a944f7c3 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -52,15 +52,6 @@ func main() { InitDefaultLogging(debug) Info.Printf("WeaveDNS version %s\n", version) // first thing in log: the version - var zone = weavedns.NewZoneDb(domain) - - if watch { - err := updater.Start(apiPath, zone) - if err != nil { - Error.Fatal("Unable to start watcher", err) - } - } - var iface *net.Interface if ifaceName != "" { var err error @@ -73,6 +64,27 @@ func main() { } } + zoneConfig := weavedns.ZoneConfig{ + Domain: domain, + Iface: iface, + } + zone, err := weavedns.NewZoneDb(zoneConfig) + if err != nil { + Error.Fatal("[main] Unable to initialize the Zone database", err) + } + err = zone.Start() + if err != nil { + Error.Fatal("[main] Unable to start the Zone database", err) + } + defer zone.Stop() + + if watch { + err := updater.Start(apiPath, zone) + if err != nil { + Error.Fatal("[main] Unable to start watcher", err) + } + } + srvConfig := weavedns.DNSServerConfig{ Port: dnsPort, CacheLen: cacheLen, From ea808bd08ba3fe4edf883863a550d1ea23eca328 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 14:09:05 +0200 Subject: [PATCH 03/21] Domain lookups, using mDNS Added DomainLookup* to the Zone interface --- nameserver/zone.go | 4 ++ nameserver/zone_lookup.go | 128 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/nameserver/zone.go b/nameserver/zone.go index b7360fd8..95ab4809 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -48,6 +48,10 @@ type Zone interface { DeleteRecord(ident string, ip net.IP) error // Delete all records for an ident in the local database DeleteRecordsFor(ident string) error + // Lookup for a name in the whole domain + DomainLookupName(name string) ([]ZoneRecord, error) + // Lookup for an address in the whole domain + DomainLookupInaddr(inaddr string) ([]ZoneRecord, error) // Return a status string Status() string } diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go index 985f84ae..f64c7fb3 100644 --- a/nameserver/zone_lookup.go +++ b/nameserver/zone_lookup.go @@ -28,6 +28,67 @@ func (zone *zoneDb) LookupName(name string) (res []ZoneRecord, err error) { return } +// 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() + for identName, nameset := range zone.idents { + for _, ze := range nameset.getEntriesForName(name) { + // filter the entries with expired TTL + // locally introduced entries are nver expired as the always have TTL=0 + if ze.hasExpired(now) { + Debug.Printf("[zonedb] '%s': expired entry '%s' ignored: removing", name, ze) + nameset.deleteName(name) + } else { + res = append(res, ze) + } + } + if identName != defaultRemoteIdent { + nameset.touchName(name, now) + } + } + zone.mx.RUnlock() + + if len(res) > 0 { + Debug.Printf("[zonedb] '%s' resolved in local database", name) + } else { + // no local results have been obtained in the local database: try with a mDNS query + // (this request is not a background query, so we cannot delay the response) + Debug.Printf("[zonedb] name '%s' not in local database: trying with mDNS", name) + ips, err := zone.mdnsCli.LookupName(name) + if err != nil { + Debug.Printf("[zonedb] mDNS lookup error for '%s': %s", name, 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'", ips, name) + res = make([]ZoneRecord, len(ips)) + zone.mx.Lock() + now = zone.clock.Now() + for i, zr := range ips { + res[i], err = zone.getNamesSet(defaultRemoteIdent).addIPToName(zr, now) + if err != nil { + zone.mx.Unlock() + Warning.Printf("[zonedb] IP [%s] insertion for '%s' failed: %s", ips, name, err) + return nil, err + } + } + zone.mx.Unlock() + } + + if len(res) > 0 { + return res, nil + } + + return nil, LookupError(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) { @@ -53,3 +114,70 @@ func (zone *zoneDb) LookupInaddr(inaddr string) (res []ZoneRecord, err error) { } return } + +// 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() + for identName, nameset := range zone.idents { + for _, ze := range nameset.getEntriesForIP(revIPv4) { + // filter the entries with expired TTL + // locally introduced entries are nver expired as the always have TTL=0 + if ze.hasExpired(now) { + Debug.Printf("[zonedb] '%s': expired entry '%s' ignored: removing", revIPv4, ze) + nameset.deleteIP(revIPv4) + } else { + res = append(res, ZoneRecord(ze)) + if identName != defaultRemoteIdent { + nameset.touchName(ze.Name(), now) + } + } + } + } + zone.mx.RUnlock() + + if len(res) > 0 { + Debug.Printf("[zonedb] '%s' resolved in local database", inaddr) + } else { + // no local results have been obtained in the local database: try with a mDNS query + Debug.Printf("[zonedb] '%s'(%+v) not in local database... trying with mDNS", inaddr, revIPv4) + names, err := zone.mdnsCli.LookupInaddr(inaddr) + if err != nil { + Debug.Printf("[zonedb] mDNS lookup error for '%s': %s", inaddr, 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'", names, revIPv4) + res = make([]ZoneRecord, len(names)) + zone.mx.Lock() + now = zone.clock.Now() + for i, name := range names { + res[i], err = zone.getNamesSet(defaultRemoteIdent).addIPToName(name, now) + if err != nil { + zone.mx.Unlock() + Warning.Printf("[zonedb] Name '%s' insertion for %s failed: %s", name.Name(), revIPv4, err) + return nil, err + } + } + zone.mx.Unlock() + } + + if len(res) > 0 { + return res, nil + } + + return nil, LookupError(inaddr) + +} + From dbce56ed30d71dbe921cf7f0b96972a4e2321295 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Thu, 30 Apr 2015 12:56:58 +0200 Subject: [PATCH 04/21] Background updates for names --- nameserver/zone.go | 120 +++++++++++++++++++++++++++++- nameserver/zone_lookup.go | 149 ++++++++++++++++++++++++++++++++++++++ weavedns/main.go | 35 +++++---- 3 files changed, 287 insertions(+), 17 deletions(-) diff --git a/nameserver/zone.go b/nameserver/zone.go index 95ab4809..c863ddf0 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -17,7 +17,11 @@ const ( const ( DefaultLocalDomain = "weave.local." // The default name used for the local domain + DefaultRefreshInterval = int(localTTL) // Period for background updates with mDNS + DefaultRelevantTime = 60 // When to forget info about remote info if nobody asks... + defaultNumUpdaters = 1 // Number of background updaters + defaultRefreshMailbox = 1000 // Number of on-the-fly background queries defaultRemoteIdent = "weave:remote" // Ident used for info obtained from mDNS ) @@ -58,7 +62,7 @@ type Zone interface { /////////////////////////////////////////////////////////////////// // -// Zone database: +// Zone database overview: // // idents names entry // *-------* *------* *------* @@ -148,6 +152,7 @@ type name struct { name string ipv4 map[IPv4]*recordEntry // all the IPv4 records for this name observers []ZoneRecordObserver // the observers for this name + lastRefreshTime time.Time // last time this name was updated by broadcasting a query lastAccessTime time.Time // last time a Lookup* used info from this name } @@ -224,6 +229,7 @@ func (n *name) updateIPs(records []ZoneRecord, now time.Time) (added int, remove // notify observers and update times if added > 0 || removed > 0 { + n.lastRefreshTime = now n.notifyNameObservers() } return @@ -404,11 +410,23 @@ func (irs identRecordsSet) String() string { /////////////////////////////////////////////////////////////////// +// Invariant on updateRequests: they are in ascending order of time. +type refreshRequest struct { + name string + time time.Time +} + type ZoneConfig struct { // The local domain Domain string // The interface where mDNS operates Iface *net.Interface + // Refresh interval for local names in the zone database (disabled by default) (in seconds) + RefreshInterval int + // Forget about remote info if nobody asks for it in this time (in seconds) + RelevantTime int + // Max pending refresh requests + MaxRefreshRequests int // Force a specific mDNS client MDNSClient ZoneMDNSClient // Force a specific mDNS server @@ -429,11 +447,23 @@ type zoneDb struct { mdnsSrv ZoneMDNSServer iface *net.Interface domain string // the local domain + relevantLimit time.Duration // if no one asks for something in this time, it is not relevant a anymore... clock clock.Clock + + refreshChan chan refreshRequest // channel for enqueing requests for updating names + refreshSchedChan chan refreshRequest // channel for enqueing requests for updating names + refreshCloseChan chan bool + refreshWg sync.WaitGroup + refreshInterval time.Duration } // Create a new zone database func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { + maxRefreshRequests := defaultRefreshMailbox + if config.MaxRefreshRequests > 0 { + maxRefreshRequests = config.MaxRefreshRequests + } + zone = &zoneDb{ domain: config.Domain, idents: make(identRecordsSet), @@ -441,6 +471,10 @@ func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { mdnsSrv: config.MDNSServer, iface: config.Iface, clock: config.Clock, + relevantLimit: time.Duration(DefaultRelevantTime) * time.Second, + refreshChan: make(chan refreshRequest, maxRefreshRequests), + refreshSchedChan: make(chan refreshRequest, maxRefreshRequests), + refreshCloseChan: make(chan bool), } // fix the default configuration parameters @@ -450,6 +484,12 @@ func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { if len(zone.domain) == 0 { zone.domain = DefaultLocalDomain } + if config.RefreshInterval > 0 { + zone.refreshInterval = time.Duration(config.RefreshInterval) * time.Second + } + if config.RelevantTime > 0 { + zone.relevantLimit = time.Duration(config.RelevantTime) * time.Second + } // Create the mDNS client and server if zone.mdnsCli == nil { @@ -469,7 +509,7 @@ func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { // Start the zone database func (zone *zoneDb) Start() (err error) { if zone.iface != nil { - Info.Printf("[zonedb] Using mDNS on %s", zone.iface) + Info.Printf("[zonedb] Using mDNS on %+v", zone.iface) } else { Info.Printf("[zonedb] Using mDNS on all interfaces") } @@ -480,11 +520,34 @@ func (zone *zoneDb) Start() (err error) { if err = zone.mdnsSrv.Start(zone.iface); err != nil { return } + + // Start some name refreshers... + if zone.refreshInterval > 0 { + Info.Printf("[zonedb] Starting %d background name updaters", defaultNumUpdaters) + for i := 0; i < defaultNumUpdaters; i++ { + zone.refreshWg.Add(1) + go zone.updater(i) + } + + zone.refreshWg.Add(1) + go zone.periodicUpdater() + } else { + // TODO: the refresher also garbage collects irrelevant remote info... we should have an alternative mechanism + // when we don't use refreshers. + Warning.Printf("[zonedb] Remote info will not be garbage collected") + } + return } // Perform a graceful shutdown of the zone database func (zone *zoneDb) Stop() error { + if zone.refreshInterval > 0 { + Debug.Printf("[zonedb] Closing background updaters...") + close(zone.refreshCloseChan) + zone.refreshWg.Wait() + } + Debug.Printf("[zonedb] Exiting mDNS client and server...") zone.mdnsCli.Stop() zone.mdnsSrv.Stop() @@ -598,7 +661,7 @@ func (zone *zoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (e } // Get the string representation of a zone -func (zone zoneDb) String() string { +func (zone *zoneDb) String() string { zone.mx.RLock() defer zone.mx.RUnlock() return zone.idents.String() @@ -610,6 +673,57 @@ func (zone *zoneDb) ContainerDied(ident string) error { return zone.DeleteRecordsFor(ident) } +// Return true if a remote name is still relevant +func (zone *zoneDb) IsNameRelevant(n string) bool { + zone.mx.Lock() + defer zone.mx.Unlock() + + lastAccess := time.Time{} + for _, nameset := range zone.idents { + if t := nameset.getNameLastAccess(n); !t.IsZero() { + if lastAccess.IsZero() || t.After(lastAccess) { + lastAccess = t + } + } + } + + return !lastAccess.IsZero() && zone.clock.Now().Sub(lastAccess) < zone.relevantLimit +} + +// Returns true if the info we have for a remote name has expired. Local names are never expired +// Returns false if we do not have info for this name from remote peers. +func (zone *zoneDb) IsNameExpired(n string) bool { + zone.mx.Lock() + defer zone.mx.Unlock() + + now := zone.clock.Now() + remoteNamesSet := zone.getNamesSet(defaultRemoteIdent) + entries := remoteNamesSet.getEntriesForName(n) + if entries != nil { + for _, entry := range entries { + if entry.hasExpired(now) { + return true + } + } + } + return false +} + +func (zone *zoneDb) HasNameLocalInfo(n string) bool { + zone.mx.Lock() + defer zone.mx.Unlock() + + remoteNamesSet := zone.getNamesSet(defaultRemoteIdent) + for _, nameset := range zone.idents { + if nameset != remoteNamesSet { + if nameset.hasName(n) { + return true + } + } + } + return false +} + // Return true if the we have obtained some information for a name from remote peers // We can have both local (eg, introduced through the HTTP API) and remote (eg, mDNS) // information for a name. This method only checks if there is remote information... diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go index f64c7fb3..247df6bd 100644 --- a/nameserver/zone_lookup.go +++ b/nameserver/zone_lookup.go @@ -3,6 +3,7 @@ package nameserver import ( "github.com/miekg/dns" . "github.com/weaveworks/weave/common" + "time" ) // Perform a lookup for a name in the zone @@ -83,6 +84,10 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) } if len(res) > 0 { + // start (if we need to) a background refresh for the name + // note: we do not spend time trying to update names that did not return an initial response... + zone.startUpdatingName(name) + return res, nil } @@ -174,6 +179,13 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err } 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 _, name := range res { + zone.startUpdatingName(name.Name()) + } + return res, nil } @@ -181,3 +193,140 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err } +////////////////////////////////////////////////////////////////////////////// + +// 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 + } + } +} diff --git a/weavedns/main.go b/weavedns/main.go index a944f7c3..f5b7e1f2 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -15,19 +15,21 @@ var version = "(unreleased version)" func main() { var ( - justVersion bool - ifaceName string - apiPath string - domain string - dnsPort int - httpPort int - wait int - timeout int - udpbuf int - cacheLen int - watch bool - debug bool - err error + justVersion bool + ifaceName string + apiPath string + domain string + dnsPort int + httpPort int + wait int + timeout int + udpbuf int + refreshInterval int + relevantTime int + cacheLen int + watch bool + debug bool + err error ) flag.BoolVar(&justVersion, "version", false, "print version and exit") @@ -42,6 +44,9 @@ func main() { flag.IntVar(&cacheLen, "cache", weavedns.DefaultCacheLen, "cache length") flag.BoolVar(&watch, "watch", true, "watch the docker socket for container events") flag.BoolVar(&debug, "debug", false, "output debugging info to stderr") + // advanced options + flag.IntVar(&refreshInterval, "refresh", weavedns.DefaultRefreshInterval, "refresh interval (in secs) for local names (0=disable)") + flag.IntVar(&relevantTime, "relevant", weavedns.DefaultRelevantTime, "life time for info in the absence of queries (in secs)") flag.Parse() if justVersion { @@ -50,7 +55,7 @@ func main() { } InitDefaultLogging(debug) - Info.Printf("WeaveDNS version %s\n", version) // first thing in log: the version + Info.Printf("[main] WeaveDNS version %s\n", version) // first thing in log: the version var iface *net.Interface if ifaceName != "" { @@ -67,6 +72,8 @@ func main() { zoneConfig := weavedns.ZoneConfig{ Domain: domain, Iface: iface, + RefreshInterval: refreshInterval, + RelevantTime: relevantTime, } zone, err := weavedns.NewZoneDb(zoneConfig) if err != nil { From 64090d0b956872f7befd677184d3eb65ed8cecd0 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 27 Apr 2015 13:37:36 +0200 Subject: [PATCH 05/21] Improved mocks --- nameserver/mocks_test.go | 390 ++++++++++++++++++++++++++++++++++++++ nameserver/server_test.go | 94 ++------- 2 files changed, 402 insertions(+), 82 deletions(-) diff --git a/nameserver/mocks_test.go b/nameserver/mocks_test.go index 5f3ba870..55fabdcb 100644 --- a/nameserver/mocks_test.go +++ b/nameserver/mocks_test.go @@ -1,9 +1,15 @@ package nameserver import ( + "fmt" + "github.com/miekg/dns" . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" "net" + "strconv" "sync" + "testing" + "time" ) const ( @@ -89,3 +95,387 @@ func (mz *mockedZoneWithRecords) ObserveInaddr(inaddr string, observer ZoneRecor notImplWarn() return nil } + +////////////////////////////////////////////////////////////////// + +// A mocked mDNS server +type mockedMDNSServer struct { + zone Zone +} + +func newMockedMDNSServer(z Zone) *mockedMDNSServer { return &mockedMDNSServer{zone: z} } +func (ms *mockedMDNSServer) Start(_ *net.Interface) error { return nil } +func (ms *mockedMDNSServer) Stop() error { return nil } +func (ms *mockedMDNSServer) Zone() Zone { return ms.zone } + +// a useful mix of NewMockedZone and NewMockedMDNSServer +func newMockedMDNSServerWithRecord(zr ZoneRecord) *mockedMDNSServer { + return newMockedMDNSServer(newMockedZoneWithRecords([]ZoneRecord{zr})) +} +func newMockedMDNSServerWithRecords(zrs []ZoneRecord) *mockedMDNSServer { + return newMockedMDNSServer(newMockedZoneWithRecords(zrs)) +} + +////////////////////////////////////////////////////////////////// + +// A mocked mDNS client. +// This mock asks a group of (potentially mocked) mDNS servers +type mockedMDNSClient struct { + sync.RWMutex + + servers map[ZoneMDNSServer]ZoneMDNSServer + + // Statistics + NumLookupsName int + NumLookupsInaddr int + NumInsLookupsName int + NumInsLookupsInaddr int +} + +func newMockedMDNSClient(srvrs []*mockedMDNSServer) *mockedMDNSClient { + r := mockedMDNSClient{ + servers: make(map[ZoneMDNSServer]ZoneMDNSServer), + } + if srvrs != nil { + for _, server := range srvrs { + r.servers[server] = server + } + } + return &r +} + +func (mc *mockedMDNSClient) Start(ifi *net.Interface) error { return nil } +func (mc *mockedMDNSClient) Stop() error { return nil } +func (mc *mockedMDNSClient) Domain() string { return DefaultLocalDomain } +func (mc *mockedMDNSClient) LookupName(name string) ([]ZoneRecord, error) { + mc.Lock() + defer mc.Unlock() + + // get a random result + mc.NumLookupsName += 1 + for _, server := range mc.servers { + r, err := server.Zone().LookupName(name) + if err == nil { + return r, nil // return the first answer + } + } + return nil, LookupError(name) +} + +func (mc *mockedMDNSClient) LookupInaddr(inaddr string) ([]ZoneRecord, error) { + mc.Lock() + defer mc.Unlock() + + // get a random result + mc.NumLookupsInaddr += 1 + for _, server := range mc.servers { + r, err := server.Zone().LookupInaddr(inaddr) + if err == nil { + return r, nil // return the first answer + } + } + return nil, LookupError(inaddr) +} + +func (mc *mockedMDNSClient) InsistentLookupName(name string) ([]ZoneRecord, error) { + mc.Lock() + defer mc.Unlock() + + res := make([]ZoneRecord, 0) + mc.NumInsLookupsName += 1 + for _, server := range mc.servers { + r, err := server.Zone().LookupName(name) + if err == nil { + res = append(res, r...) + } + } + if len(res) == 0 { + return nil, LookupError(name) + } + return res, nil +} + +func (mc *mockedMDNSClient) InsistentLookupInaddr(inaddr string) ([]ZoneRecord, error) { + mc.Lock() + defer mc.Unlock() + + res := make([]ZoneRecord, 0) + mc.NumInsLookupsInaddr += 1 + for _, server := range mc.servers { + r, err := server.Zone().LookupInaddr(inaddr) + if err == nil { + res = append(res, r...) + } + } + if len(res) == 0 { + return nil, LookupError(inaddr) + } + return res, nil +} + +func (mc *mockedMDNSClient) AddServer(srv ZoneMDNSServer) { + mc.Lock() + defer mc.Unlock() + mc.servers[srv] = srv +} + +func (mc *mockedMDNSClient) RemoveServer(srv ZoneMDNSServer) { + mc.Lock() + defer mc.Unlock() + delete(mc.servers, srv) +} + +////////////////////////////////////////////////////////////////// + +type zbmEntry struct { + Server *mockedMDNSServer + Client *mockedMDNSClient + Zone *zoneDb +} +type zoneDbsWithMockedMDns []*zbmEntry + +// Creates a group of (real) zone databases linked through mocked mDNS servers and clients +// This effectively creates a cluster of WeaveDNS peers +func newZoneDbsWithMockedMDns(num int, config ZoneConfig) zoneDbsWithMockedMDns { + res := make(zoneDbsWithMockedMDns, num) + + for i := 0; i < num; i++ { + res[i] = new(zbmEntry) + + Debug.Printf("[test] Creating mocked mDNS server #%d", i) + res[i].Server = newMockedMDNSServer(nil) + + Debug.Printf("[test] Creating mocked mDNS client #%d", i) + res[i].Client = newMockedMDNSClient([]*mockedMDNSServer{}) + + cfg := config + cfg.MDNSClient = res[i].Client + cfg.MDNSServer = res[i].Server + + Debug.Printf("[test] Creating ZoneDb #%d", i) + res[i].Zone, _ = NewZoneDb(cfg) + + // link the mDNS server to its zone + // ZoneDbs are connected to mDNS in two directions: they use their mDNS + // clients for asking other peers about names/IPs, and export their records + // through a mDNS server. + res[i].Server.zone = res[i].Zone + } + + // link all the clients to all the servers + for i := 0; i < num; i++ { + for j := 0; j < num; j++ { + // with this check, the local client will not ask to the local server + // this is not what we currently do, but can be convenient in some cases + // for finding some bugs... + if i != j { + Debug.Printf("[test] Linking mocked mDNS client #%d to server #%d", i, j) + res[i].Client.AddServer(res[j].Server) + } + } + } + + return res +} + +func (zbs zoneDbsWithMockedMDns) Start() { + for _, entry := range zbs { + entry.Zone.Start() + } +} + +func (zbs zoneDbsWithMockedMDns) Stop() { + for _, entry := range zbs { + entry.Zone.Stop() + } +} + +////////////////////////////////////////////////////////////////// + +// A mocked cache where we never find a single thing... ;) +type mockedCache struct { + NumGets int + NumPuts int + NumRemovals int +} + +func newMockedCache() *mockedCache { return &mockedCache{} } + +func (c *mockedCache) Get(request *dns.Msg, maxLen int) (reply *dns.Msg, err error) { + c.NumGets += 1 + return nil, nil +} +func (c *mockedCache) Put(request *dns.Msg, reply *dns.Msg, ttl int, flags uint8) int { + c.NumPuts += 1 + return 0 +} +func (c *mockedCache) Remove(question *dns.Question) { c.NumRemovals += 1 } +func (c *mockedCache) Purge() {} +func (c *mockedCache) Clear() {} +func (c *mockedCache) Len() int { return 0 } +func (c *mockedCache) Capacity() int { return DefaultCacheLen } + +////////////////////////////////////////////////////////////////// + +// A mocked fallback server +type mockedFallback struct { + CliConfig *dns.ClientConfig + Addr string + Port int + + udpSrv *dns.Server + tcpSrv *dns.Server +} + +// Create a mocked fallback server +// You can use the server's `CliConfig` as the `UpstreamCfg` of a `DNSServer`... +func newMockedFallback(udpH dns.HandlerFunc, tcpH dns.HandlerFunc) (*mockedFallback, error) { + udpSrv, fallbackAddr, err := runLocalUDPServer("127.0.0.1:0", udpH) + if err != nil { + return nil, err + } + _, fallbackPort, err := net.SplitHostPort(fallbackAddr) + if err != nil { + return nil, err + } + + fallbackPortI, _ := strconv.Atoi(fallbackPort) + + res := mockedFallback{ + udpSrv: udpSrv, + Addr: fallbackAddr, + Port: fallbackPortI, + CliConfig: &dns.ClientConfig{ + Servers: []string{"127.0.0.1"}, + Port: fallbackPort, + }, + } + + if tcpH != nil { + fallbackTCPAddr := fmt.Sprintf("127.0.0.1:%s", fallbackPort) + res.tcpSrv, _, err = runLocalTCPServer(fallbackTCPAddr, tcpH) + if err != nil { + return nil, err + } + } + + return &res, nil +} + +// Start the fallback server +func (mf *mockedFallback) Start() error { + return nil +} + +// Stop the fallback server +func (mf *mockedFallback) Stop() error { + if mf.tcpSrv != nil { + mf.udpSrv.Shutdown() + mf.udpSrv = nil + } + if mf.tcpSrv != nil { + mf.tcpSrv.Shutdown() + mf.udpSrv = nil + } + return nil +} + +// Run a UDP fallback server +func runLocalUDPServer(laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) { + Debug.Printf("[mocked fallback] Starting fallback UDP server at %s", laddr) + pc, err := net.ListenPacket("udp", laddr) + if err != nil { + return nil, "", err + } + server := &dns.Server{PacketConn: pc, Handler: handler, ReadTimeout: testSocketTimeout * time.Millisecond} + + go func() { + server.ActivateAndServe() + pc.Close() + }() + + Debug.Printf("[mocked fallback] Fallback UDP server listening at %s", pc.LocalAddr()) + return server, pc.LocalAddr().String(), nil +} + +// Run a TCP fallback server +func runLocalTCPServer(laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) { + Debug.Printf("[mocked fallback] Starting fallback TCP server at %s", laddr) + laddrTCP, err := net.ResolveTCPAddr("tcp", laddr) + if err != nil { + return nil, "", err + } + + l, err := net.ListenTCP("tcp", laddrTCP) + if err != nil { + return nil, "", err + } + server := &dns.Server{Listener: l, Handler: handler, ReadTimeout: testSocketTimeout * time.Millisecond} + + go func() { + server.ActivateAndServe() + l.Close() + }() + + Debug.Printf("[mocked fallback] Fallback TCP server listening at %s", l.Addr().String()) + return server, l.Addr().String(), nil +} + +////////////////////////////////////////////////////////////////// + +// Perform a DNS query and assert the reply code, number or answers, etc +func assertExchange(t *testing.T, z string, ty uint16, port int, minAnswers int, maxAnswers int, expErr int) (*dns.Msg, *dns.Msg) { + wt.AssertNotEqualInt(t, port, 0, "invalid DNS server port") + + c := &dns.Client{ + UDPSize: testUDPBufSize, + } + + m := new(dns.Msg) + m.RecursionDesired = true + m.SetQuestion(z, ty) + m.SetEdns0(testUDPBufSize, false) // we don't want to play with truncation here... + + lstAddr := fmt.Sprintf("127.0.0.1:%d", port) + r, _, err := c.Exchange(m, lstAddr) + t.Logf("Response from '%s':\n%+v\n", lstAddr, r) + if err != nil { + t.Fatalf("Error when querying DNS server at %s: %s", lstAddr, err) + } + wt.AssertNoErr(t, err) + if minAnswers == 0 && maxAnswers == 0 { + wt.AssertStatus(t, r.Rcode, expErr, "DNS response code") + } else { + wt.AssertStatus(t, r.Rcode, dns.RcodeSuccess, "DNS response code") + } + answers := len(r.Answer) + if minAnswers >= 0 && answers < minAnswers { + wt.Fatalf(t, "Number of answers >= %d", minAnswers) + } + if maxAnswers >= 0 && answers > maxAnswers { + wt.Fatalf(t, "Number of answers <= %d", maxAnswers) + } + return m, r +} + +// Assert that we have a response in the cache for a query `q` +func assertInCache(t *testing.T, cache ZoneCache, q *dns.Msg, desc string) { + r, err := cache.Get(q, maxUDPSize) + wt.AssertNoErr(t, err) + wt.AssertNotNil(t, r, fmt.Sprintf("value in the cache: %s", desc)) +} + +// Assert that we have a response in the cache for a query `q` +func assertNotLocalInCache(t *testing.T, cache ZoneCache, q *dns.Msg, desc string) { + r, err := cache.Get(q, maxUDPSize) + if !(r == nil && err == errNoLocalReplies) { + t.Fatalf("Cache does not return a noLocalReplies error for query %s", q) + } +} + +// Assert that we do not have a response in the cache for a query `q` +func assertNotInCache(t *testing.T, cache ZoneCache, q *dns.Msg, desc string) { + r, err := cache.Get(q, maxUDPSize) + wt.AssertNoErr(t, err) + wt.AssertNil(t, r, fmt.Sprintf("value in the cache: %s\n%s", desc, r)) +} diff --git a/nameserver/server_test.go b/nameserver/server_test.go index 2ae5203f..33ff6cb0 100644 --- a/nameserver/server_test.go +++ b/nameserver/server_test.go @@ -80,7 +80,7 @@ func TestUDPDNSServer(t *testing.T) { } // Run another DNS server for fallback - s, fallbackAddr, err := runLocalUDPServer(t, "127.0.0.1:0", fallbackHandler) + s, fallbackAddr, err := runLocalUDPServer("127.0.0.1:0", fallbackHandler) wt.AssertNoErr(t, err) defer s.Shutdown() @@ -96,29 +96,29 @@ func TestUDPDNSServer(t *testing.T) { var r *dns.Msg - r = assertExchange(t, successTestName, dns.TypeA, 1, 1, 0) + _, r = assertExchange(t, successTestName, dns.TypeA, testPort, 1, 1, 0) wt.AssertType(t, r.Answer[0], (*dns.A)(nil), "DNS record") wt.AssertEqualString(t, r.Answer[0].(*dns.A).A.String(), testAddr1, "IP address") - assertExchange(t, failTestName, dns.TypeA, 0, 0, dns.RcodeNameError) + assertExchange(t, failTestName, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) - r = assertExchange(t, testRDNSsuccess, dns.TypePTR, 1, 1, 0) + _, r = assertExchange(t, testRDNSsuccess, dns.TypePTR, testPort, 1, 1, 0) wt.AssertType(t, r.Answer[0], (*dns.PTR)(nil), "DNS record") wt.AssertEqualString(t, r.Answer[0].(*dns.PTR).Ptr, successTestName, "IP address") - assertExchange(t, testRDNSfail, dns.TypePTR, 0, 0, dns.RcodeNameError) + assertExchange(t, testRDNSfail, dns.TypePTR, testPort, 0, 0, dns.RcodeNameError) - // This should fail because we don't have information about MX records - assertExchange(t, successTestName, dns.TypeMX, 0, 0, dns.RcodeNameError) + // This should fail because we don't handle MX records + assertExchange(t, successTestName, dns.TypeMX, testPort, 0, 0, dns.RcodeNameError) // This non-local query for an MX record should succeed by being // passed on to the fallback server - assertExchange(t, nonLocalName, dns.TypeMX, 1, -1, 0) + assertExchange(t, nonLocalName, dns.TypeMX, testPort, 1, -1, 0) // Now ask a query that we expect to return a lot of data. - assertExchange(t, nonLocalName, dns.TypeANY, 5, -1, 0) + assertExchange(t, nonLocalName, dns.TypeANY, testPort, 5, -1, 0) - assertExchange(t, testRDNSnonlocal, dns.TypePTR, 1, -1, 0) + assertExchange(t, testRDNSnonlocal, dns.TypePTR, testPort, 1, -1, 0) // Not testing MDNS functionality of server here (yet), since it // needs two servers, each listening on its own address @@ -173,7 +173,7 @@ func TestTCPDNSServer(t *testing.T) { } t.Logf("Running a DNS fallback server with UDP") - us, fallbackUDPAddr, err := runLocalUDPServer(t, "127.0.0.1:0", fallbackUDPHandler) + us, fallbackUDPAddr, err := runLocalUDPServer("127.0.0.1:0", fallbackUDPHandler) wt.AssertNoErr(t, err) defer us.Shutdown() @@ -182,7 +182,7 @@ func TestTCPDNSServer(t *testing.T) { t.Logf("Starting another fallback server, with TCP, on the same port as the UDP server") fallbackTCPAddr := fmt.Sprintf("127.0.0.1:%s", fallbackPort) - ts, fallbackTCPAddr, err := runLocalTCPServer(t, fallbackTCPAddr, fallbackTCPHandler) + ts, fallbackTCPAddr, err := runLocalTCPServer(fallbackTCPAddr, fallbackTCPHandler) wt.AssertNoErr(t, err) defer ts.Shutdown() @@ -242,73 +242,3 @@ func TestTCPDNSServer(t *testing.T) { wt.AssertFalse(t, r.MsgHdr.Truncated, "DNS truncated response flag") wt.AssertEqualInt(t, len(r.Answer), numAnswers, "number of answers (UDP-long)") } - -////////////////////////////////////////////////////////////////////////////// - -// perform a DNS query and assert the reply code, number or answers, etc -func assertExchange(t *testing.T, z string, ty uint16, minAnswers int, maxAnswers int, expErr int) *dns.Msg { - c := new(dns.Client) - c.UDPSize = testUDPBufSize - m := new(dns.Msg) - m.RecursionDesired = true - m.SetQuestion(z, ty) - m.SetEdns0(testUDPBufSize, false) // we don't want to play with truncation here... - - r, _, err := c.Exchange(m, fmt.Sprintf("127.0.0.1:%d", testPort)) - t.Logf("Response:\n%+v\n", r) - wt.AssertNoErr(t, err) - if minAnswers == 0 && maxAnswers == 0 { - wt.AssertStatus(t, r.Rcode, expErr, "DNS response code") - } else { - wt.AssertStatus(t, r.Rcode, dns.RcodeSuccess, "DNS response code") - } - answers := len(r.Answer) - if minAnswers >= 0 && answers < minAnswers { - wt.Fatalf(t, "Number of answers >= %d", minAnswers) - } - if maxAnswers >= 0 && answers > maxAnswers { - wt.Fatalf(t, "Number of answers <= %d", maxAnswers) - } - return r -} - -// run a UDP fallback server -func runLocalUDPServer(t *testing.T, laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) { - t.Logf("Starting fallback UDP server at %s", laddr) - pc, err := net.ListenPacket("udp", laddr) - if err != nil { - return nil, "", err - } - server := &dns.Server{PacketConn: pc, Handler: handler, ReadTimeout: 100 * time.Millisecond} - - go func() { - server.ActivateAndServe() - pc.Close() - }() - - t.Logf("Fallback UDP server listening at %s", pc.LocalAddr()) - return server, pc.LocalAddr().String(), nil -} - -// run a TCP fallback server -func runLocalTCPServer(t *testing.T, laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) { - t.Logf("Starting fallback TCP server at %s", laddr) - laddrTCP, err := net.ResolveTCPAddr("tcp", laddr) - if err != nil { - return nil, "", err - } - - l, err := net.ListenTCP("tcp", laddrTCP) - if err != nil { - return nil, "", err - } - server := &dns.Server{Listener: l, Handler: handler, ReadTimeout: 100 * time.Millisecond} - - go func() { - server.ActivateAndServe() - l.Close() - }() - - t.Logf("Fallback TCP server listening at %s", l.Addr().String()) - return server, l.Addr().String(), nil -} From 6fc6afa6a7c6e5a1baf05a2b6f4a56881da9f801 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 27 Apr 2015 13:38:03 +0200 Subject: [PATCH 06/21] Unit tests for the zone lookups and names updates --- nameserver/zone_lookup_test.go | 114 +++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 nameserver/zone_lookup_test.go diff --git a/nameserver/zone_lookup_test.go b/nameserver/zone_lookup_test.go new file mode 100644 index 00000000..8593086b --- /dev/null +++ b/nameserver/zone_lookup_test.go @@ -0,0 +1,114 @@ +package nameserver + +import ( + "github.com/benbjohnson/clock" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" + "net" + "testing" + "time" +) + +// Check that the refreshing mechanism works as expected +func TestZoneRefresh(t *testing.T) { + const ( + refreshInterval = 5 + relevantTime = 30 + ) + var ( + name = "mysql.weave.local." + addr1 = "10.2.8.4" + addr2 = "10.2.8.5" + addr3 = "10.2.8.6" + addr4 = "10.2.8.7" + ) + + InitDefaultLogging(testing.Verbose()) + Info.Println("TestZoneRefresh starting") + + clk := clock.NewMock() + forwardClock := func(secs int) { + Debug.Printf(">>>>>>> Moving clock forward %d seconds - Time traveling >>>>>>>", secs) + clk.Add(time.Duration(secs) * time.Second) + Debug.Printf("<<<<<<< Time travel finished! We are at %s <<<<<<<", clk.Now()) + } + + // Create multiple zone databases, linked through mocked mDNS connections + zc := ZoneConfig{ + RefreshInterval: refreshInterval, + RelevantTime: relevantTime, + Clock: clk, + } + dbs := newZoneDbsWithMockedMDns(2, zc) + dbs.Start() + defer dbs.Stop() + + time.Sleep(100 * time.Millisecond) // allow for server to get going + + Debug.Printf("Adding '%s' to Db #1", name) + dbs[0].Zone.AddRecord("someident", name, net.ParseIP(addr1)) + + Debug.Printf("Checking that the name %s is relevant (as it has been locally inserted) and not remote", name) + wt.AssertTrue(t, dbs[0].Zone.IsNameRelevant(name), "name relevant") + wt.AssertTrue(t, dbs[0].Zone.HasNameLocalInfo(name), "local name info") + wt.AssertFalse(t, dbs[0].Zone.HasNameRemoteInfo(name), "remote name info") + + Debug.Printf("Asking for '%s' to Db #1: should get 1 IP from the local database...", name) + res, err := dbs[0].Zone.DomainLookupName(name) + wt.AssertNoErr(t, err) + Debug.Printf("Got: %s", res) + t.Logf("Db #1 after the lookup:\n%s", dbs[0].Zone) + wt.AssertEqualInt(t, len(res), 1, "lookup result") + + forwardClock(refreshInterval / 2) + Debug.Printf("A couple of seconds later, we should still have one IP for that name") + res, err = dbs[0].Zone.DomainLookupName(name) + wt.AssertNoErr(t, err) + wt.AssertEqualInt(t, len(res), 1, "lookup result") + + Debug.Printf("And then we add 2 IPs for that name at ZoneDb 2") + forwardClock(1) + Debug.Printf("Adding 2 IPs to '%s' in Db #2", name) + dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr2)) + dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr3)) + + Debug.Printf("Perform a lookup, to ensure the name will be updated in the background...") + dbs[0].Zone.DomainLookupName(name) + + Debug.Printf("Wait for a while, until a refresh is performed...") + forwardClock(refreshInterval + 1) + + Debug.Printf("A refresh should have been scheduled now: we should have 3 IPs:") + Debug.Printf("the first (local) IP and the others obtained from zone2 with a mDNS query") + Debug.Printf("Asking for '%s' again... we should have 3 IPs now", name) + res, err = dbs[0].Zone.DomainLookupName(name) + Debug.Printf("Got: %s", res) + t.Logf("Db #1 after the second lookup:\n%s", dbs[0].Zone) + wt.AssertEqualInt(t, len(res), 3, "lookup result length") + + Debug.Printf("We will not ask for `name` for a while, so it will become irrelevant and will be removed...") + forwardClock(relevantTime + refreshInterval + 1) + + // the name should be irrelevant now, and all remote info should have been + // removed from the zone database + Debug.Printf("Name '%s' should not be in the remote database in ZoneDb 1", name) + wt.AssertFalse(t, dbs[0].Zone.IsNameRelevant(name), "name still relevant after some inactivity time") + wt.AssertTrue(t, dbs[0].Zone.HasNameLocalInfo(name), "local name info") + wt.AssertFalse(t, dbs[0].Zone.HasNameRemoteInfo(name), "remote name info") + + Debug.Printf("There is no remote info about this name at zone 1: a new IP appears remotely meanwhile...") + forwardClock(1) + Debug.Printf("Adding '%s' to Db #2", name) + dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr4)) + + Debug.Printf("When we ask about this name again, we get 4 IPs (1 local, 3 remote)") + forwardClock(1) + Debug.Printf("Asking for '%s' again... the first lookup will return only the local results", name) + res, err = dbs[0].Zone.DomainLookupName(name) + wt.AssertEqualInt(t, len(res), 1, "lookup result length") + Debug.Printf("... but a second lookup should return all the results in the network") + forwardClock(1) + res, err = dbs[0].Zone.DomainLookupName(name) + Debug.Printf("Got: %s", res) + wt.AssertEqualInt(t, len(res), 4, "lookup result length") // TODO: this fails 1% of the runs... !!?? +} From a23462e83088aa7774c2530b101c37a0a8f219a7 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 10:54:07 +0200 Subject: [PATCH 07/21] Command line option for disabling the cache --- weavedns/main.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/weavedns/main.go b/weavedns/main.go index f5b7e1f2..840a7a09 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -27,6 +27,7 @@ func main() { refreshInterval int relevantTime int cacheLen int + cacheDisabled bool watch bool debug bool err error @@ -39,14 +40,16 @@ func main() { flag.IntVar(&wait, "wait", 0, "number of seconds to wait for interface to be created and come up") flag.IntVar(&dnsPort, "dnsport", weavedns.DefaultServerPort, "port to listen to DNS requests") flag.IntVar(&httpPort, "httpport", 6785, "port to listen to HTTP requests") - flag.IntVar(&timeout, "timeout", weavedns.DefaultTimeout, "timeout for resolutions") - flag.IntVar(&udpbuf, "udpbuf", weavedns.DefaultUDPBuflen, "UDP buffer length") flag.IntVar(&cacheLen, "cache", weavedns.DefaultCacheLen, "cache length") flag.BoolVar(&watch, "watch", true, "watch the docker socket for container events") flag.BoolVar(&debug, "debug", false, "output debugging info to stderr") // advanced options flag.IntVar(&refreshInterval, "refresh", weavedns.DefaultRefreshInterval, "refresh interval (in secs) for local names (0=disable)") flag.IntVar(&relevantTime, "relevant", weavedns.DefaultRelevantTime, "life time for info in the absence of queries (in secs)") + flag.IntVar(&udpbuf, "udpbuf", weavedns.DefaultUDPBuflen, "UDP buffer length") + flag.IntVar(&timeout, "timeout", weavedns.DefaultTimeout, "timeout for resolutions (in millisecs)") + flag.BoolVar(&cacheDisabled, "no-cache", false, "disable the cache") + flag.Parse() if justVersion { @@ -93,18 +96,16 @@ func main() { } srvConfig := weavedns.DNSServerConfig{ - Port: dnsPort, - CacheLen: cacheLen, - LocalDomain: domain, - Timeout: timeout, - UDPBufLen: udpbuf, + Port: dnsPort, + CacheLen: cacheLen, + Timeout: timeout, + UDPBufLen: udpbuf, } srv, err := weavedns.NewDNSServer(srvConfig, zone, iface) if err != nil { - Error.Fatal("Failed to initialize the WeaveDNS server", err) + Error.Fatal("[main] Failed to initialize the WeaveDNS server", err) } - Info.Println("Upstream", srv.Upstream) go SignalHandlerLoop(srv) go weavedns.ListenHTTP(version, srv, domain, zone, httpPort) From 07d15092aa5b2b42b671ff64229d4c525c7cae21 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 10:55:59 +0200 Subject: [PATCH 08/21] Do not invoke the mDNS lookups from the server: they are used from the Zone database now... Improved DNS server: - simplify the handlers code, removing duplicated code - more configuration parameters (eg, we can now disable the cache) - we can now bind to port 0 and let the server get a free port (so we can get rid of the mess we had in the unit tests...) --- nameserver/addrs.go | 1 + nameserver/dns.go | 2 + nameserver/server.go | 326 +++++++++++++++++++++----------------- nameserver/server_test.go | 71 +++++---- weavedns/main.go | 12 +- 5 files changed, 231 insertions(+), 181 deletions(-) diff --git a/nameserver/addrs.go b/nameserver/addrs.go index 1e315a91..6d6e3abd 100644 --- a/nameserver/addrs.go +++ b/nameserver/addrs.go @@ -32,6 +32,7 @@ type ZoneObservable interface { ObserveInaddr(inaddr string, observer ZoneRecordObserver) error } +type ZoneObserverFunc func(string, ZoneRecordObserver) error type ZoneRecordObserver func() ///////////////////////////////////////////////////////////// diff --git a/nameserver/dns.go b/nameserver/dns.go index 904a9bf2..3b8b8c57 100644 --- a/nameserver/dns.go +++ b/nameserver/dns.go @@ -33,6 +33,8 @@ func makeTruncatedReply(r *dns.Msg) *dns.Msg { return reply } +type DNSResponseBuilder func(r *dns.Msg, q *dns.Question, addrs []ZoneRecord) *dns.Msg + func makeAddressReply(r *dns.Msg, q *dns.Question, addrs []ZoneRecord) *dns.Msg { answers := make([]dns.RR, len(addrs)) header := makeHeader(r, q) diff --git a/nameserver/server.go b/nameserver/server.go index 9c066ea6..f2bc6d99 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -3,36 +3,48 @@ package nameserver import ( "bytes" "fmt" + "github.com/benbjohnson/clock" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" "net" + "strconv" "sync" + "time" ) const ( - DefaultServerPort = 53 // The default server port - DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load - DefaultUDPBuflen = 4096 // bigger than the default 512 - DefaultCacheLen = 8192 // default cache capacity - DefaultResolvWorkers = 8 // default number of resolution workers - DefaultTimeout = 5 // default timeout for DNS resolutions + DefaultServerPort = 53 // The default server port + DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load + DefaultUDPBuflen = 4096 // bigger than the default 512 + DefaultCacheLen = 8192 // default cache capacity + DefaultTimeout = 5 // default timeout for DNS resolutions ) type DNSServerConfig struct { + // The zone + Zone Zone // (Optional) client config file for resolving upstream servers UpstreamCfgFile string // (Optional) DNS client config for the fallback server(s) UpstreamCfg *dns.ClientConfig // (Optional) port number (for TCP and UDP) Port int - // (Optional) local domain (ie, "weave.local.") - LocalDomain string // (Optional) cache size CacheLen int + // (Optional) disable the cache + CacheDisabled bool // (Optional) timeout for DNS queries Timeout int // (Optional) UDP buffer length UDPBufLen int + // (Optional) force a specific cache + Cache ZoneCache + // (Optional) TTL for negative results in the local domain + CacheNegLocalTTL int + // (Optional) for a specific clock provider + Clock clock.Clock + // (Optional) Listening socket read timeout (in milliseconds) + ListenReadTimeout int } type dnsProtocol uint8 @@ -53,51 +65,64 @@ func (proto dnsProtocol) String() string { } // get a new dns.Client for a protocol -func (proto dnsProtocol) GetNewClient(bufsize int) *dns.Client { +func (proto dnsProtocol) GetNewClient(bufsize int, timeout time.Duration) *dns.Client { switch proto { case protTCP: - return &dns.Client{Net: "tcp"} + return &dns.Client{Net: "tcp", ReadTimeout: timeout} case protUDP: - return &dns.Client{Net: "udp", UDPSize: uint16(bufsize)} + return &dns.Client{Net: "udp", ReadTimeout: timeout, UDPSize: uint16(bufsize)} } return nil } +// a DNS server type DNSServer struct { - Zone Zone - Iface *net.Interface - Upstream *dns.ClientConfig - udpSrv *dns.Server - tcpSrv *dns.Server - mdnsCli *MDNSClient - mdnsSrv *MDNSServer - cache *Cache - timeout int - udpBuf int - listenersWg *sync.WaitGroup - + Zone Zone + Upstream *dns.ClientConfig Domain string // the local domain ListenAddr string // the address the server is listening at + + udpSrv *dns.Server + tcpSrv *dns.Server + pc net.PacketConn + lst net.Listener + cache ZoneCache + cacheDisabled bool + negLocalTTL int + timeout time.Duration + readTimeout time.Duration + udpBuf int + listenersWg *sync.WaitGroup + clock clock.Clock } // Creates a new DNS server -func NewDNSServer(config DNSServerConfig, zone Zone, iface *net.Interface) (s *DNSServer, err error) { +func NewDNSServer(config DNSServerConfig) (s *DNSServer, err error) { s = &DNSServer{ - Zone: zone, - Iface: iface, - listenersWg: new(sync.WaitGroup), - + Zone: config.Zone, Domain: DefaultLocalDomain, - ListenAddr: fmt.Sprintf(":%d", DefaultServerPort), + ListenAddr: fmt.Sprintf(":%d", config.Port), + + listenersWg: new(sync.WaitGroup), + timeout: time.Duration(config.Timeout) * time.Millisecond, + readTimeout: time.Duration(config.ListenReadTimeout) * time.Millisecond, + cacheDisabled: false, + negLocalTTL: negLocalTTL, + clock: config.Clock, + } + + // check some basic parameters are valid + if s.Zone == nil { + return nil, fmt.Errorf("No valid Zone provided in server initialization") + } + if len(s.Domain) == 0 { + return nil, fmt.Errorf("No valid Domain provided in server initialization") + } + if s.clock == nil { + s.clock = clock.New() } // fill empty parameters with defaults... - if config.Port != 0 { - s.ListenAddr = fmt.Sprintf(":%d", config.Port) - } - if len(config.LocalDomain) > 0 { - s.Domain = config.LocalDomain - } if config.UpstreamCfg != nil { s.Upstream = config.UpstreamCfg } else { @@ -110,88 +135,97 @@ func NewDNSServer(config DNSServerConfig, zone Zone, iface *net.Interface) (s *D } } if config.Timeout > 0 { - s.timeout = config.Timeout + s.timeout = time.Duration(config.Timeout) * time.Millisecond } if config.UDPBufLen > 0 { s.udpBuf = config.UDPBufLen } - s.mdnsCli, err = NewMDNSClient() - if err != nil { - return + if config.CacheNegLocalTTL > 0 { + s.negLocalTTL = config.CacheNegLocalTTL } - s.mdnsSrv, err = NewMDNSServer(s.Zone, false) - if err != nil { - return + if config.CacheDisabled { + s.cacheDisabled = true } - cacheLen := DefaultCacheLen - if config.CacheLen > 0 { - cacheLen = config.CacheLen + if !s.cacheDisabled { + if config.Cache != nil { + s.cache = config.Cache + } else { + cacheLen := DefaultCacheLen + if config.CacheLen > 0 { + cacheLen = config.CacheLen + } + if s.cache, err = NewCache(cacheLen, s.clock); err != nil { + return + } + } } - Debug.Printf("[dns] Initializing cache: %d entries", config.CacheLen) - s.cache, err = NewCache(cacheLen, nil) - if err != nil { - return - } - - // create two DNS request multiplexerers, depending on the protocol used by clients - // (we use the same protocol for asking upstream servers) - mux := func(proto dnsProtocol) *dns.ServeMux { - m := dns.NewServeMux() - m.HandleFunc(s.Domain, s.queryHandler(proto)) - m.HandleFunc(RDNSDomain, s.rdnsHandler(proto)) - m.HandleFunc(".", s.notUsHandler(proto)) - return m - } - - s.udpSrv = &dns.Server{Addr: s.ListenAddr, Net: "udp", Handler: mux(protUDP)} - s.tcpSrv = &dns.Server{Addr: s.ListenAddr, Net: "tcp", Handler: mux(protTCP)} return } // Start the DNS server func (s *DNSServer) Start() error { - Info.Printf("Using mDNS on %v", s.Iface) - err := s.mdnsCli.Start(s.Iface) - CheckFatal(err) - err = s.mdnsSrv.Start(s.Iface) - CheckFatal(err) + Info.Printf("[dns] Upstream server(s): %+v", s.Upstream) + if s.cacheDisabled { + Info.Printf("[dns] Cache: disabled") + } else { + Info.Printf("[dns] Cache: %d entries", s.cache.Capacity()) + } + + pc, err := net.ListenPacket("udp", s.ListenAddr) + if err != nil { + return err + } + s.pc = pc + + _, port, err := net.SplitHostPort(pc.LocalAddr().String()) + if err != nil { + return err + } + s.ListenAddr = fmt.Sprintf(":%s", port) + s.udpSrv = &dns.Server{PacketConn: s.pc, Handler: s.createMux(protUDP), ReadTimeout: s.readTimeout} + + // Bind the TCP socket at the same port, aborting otherwise + l, err := net.Listen("tcp", s.ListenAddr) + if err != nil { + s.Stop() + return err + } + s.lst = l + s.tcpSrv = &dns.Server{Listener: l, Handler: s.createMux(protTCP), ReadTimeout: s.readTimeout} s.listenersWg.Add(2) go func() { defer s.listenersWg.Done() - Debug.Printf("Listening for DNS on %s (UDP)", s.ListenAddr) - err = s.udpSrv.ListenAndServe() + Info.Printf("[dns] Listening for DNS on %s (UDP)", s.ListenAddr) + err := s.udpSrv.ActivateAndServe() CheckFatal(err) - Debug.Printf("DNS UDP server exiting...") + Debug.Printf("[dns] DNS UDP server exiting...") }() go func() { defer s.listenersWg.Done() - Debug.Printf("Listening for DNS on %s (TCP)", s.ListenAddr) - err = s.tcpSrv.ListenAndServe() + Info.Printf("[dns] Listening for DNS on %s (TCP)", s.ListenAddr) + err := s.tcpSrv.ActivateAndServe() CheckFatal(err) - Debug.Printf("DNS TCP server exiting...") + Debug.Printf("[dns] DNS TCP server exiting...") }() // Waiting for all goroutines to finish (otherwise they die as main routine dies) s.listenersWg.Wait() - Info.Printf("WeaveDNS server exiting...") + Info.Printf("[dns] Server exiting...") return nil } // Return status string func (s *DNSServer) Status() string { var buf bytes.Buffer - fmt.Fprintln(&buf, "Local domain", s.Domain) fmt.Fprintln(&buf, "Listen address", s.ListenAddr) - fmt.Fprintln(&buf, "mDNS interface", s.Iface) fmt.Fprintln(&buf, "Fallback DNS config", s.Upstream) - fmt.Fprintf(&buf, "Zone database:\n%s", s.Zone) return buf.String() } @@ -206,111 +240,110 @@ func (s *DNSServer) Stop() error { } s.listenersWg.Wait() - // shutdown the mDNS server - s.mdnsSrv.Stop() - return nil } -func (s *DNSServer) queryHandler(proto dnsProtocol) dns.HandlerFunc { - zoneLookup := func(lookup ZoneLookup, q *dns.Question, r *dns.Msg) (*dns.Msg, []ZoneRecord, error) { - ips, err := lookup.LookupName(q.Name) - if err != nil { - return nil, nil, err - } - return makeAddressReply(r, q, ips), ips, nil - } - - fallback := func(w dns.ResponseWriter, r *dns.Msg) { +// Create a multiplexer for requests +// We must create two DNS request multiplexers, depending on the protocol used by +// clients (as we use the same protocol for asking upstream servers) +func (s *DNSServer) createMux(proto dnsProtocol) *dns.ServeMux { + failFallback := func(w dns.ResponseWriter, r *dns.Msg) { w.WriteMsg(makeDNSFailResponse(r)) } - - return s.commonQueryHandler(proto, "Query", dns.TypeA, zoneLookup, fallback) -} - -func (s *DNSServer) rdnsHandler(proto dnsProtocol) dns.HandlerFunc { - zoneLookup := func(lookup ZoneLookup, q *dns.Question, r *dns.Msg) (*dns.Msg, []ZoneRecord, error) { - names, err := lookup.LookupInaddr(q.Name) - if err != nil { - return nil, nil, err - } - return makePTRReply(r, q, names), names, nil - } - notUsHandler := s.notUsHandler(proto) - fallback := func(w dns.ResponseWriter, r *dns.Msg) { + notUsFallback := func(w dns.ResponseWriter, r *dns.Msg) { Info.Printf("[dns msgid %d] -> sending to fallback server", r.MsgHdr.Id) notUsHandler(w, r) } - return s.commonQueryHandler(proto, "Reverse query", dns.TypePTR, zoneLookup, fallback) + // create the multiplexer + m := dns.NewServeMux() + m.HandleFunc(s.Zone.Domain(), s.localHandler(proto, "Query", dns.TypeA, + s.Zone.DomainLookupName, makeAddressReply, s.Zone.ObserveName, failFallback)) + m.HandleFunc(RDNSDomain, s.localHandler(proto, "Reverse query", dns.TypePTR, + s.Zone.DomainLookupInaddr, makePTRReply, s.Zone.ObserveInaddr, notUsFallback)) + m.HandleFunc(".", s.notUsHandler(proto)) + return m } -func (s *DNSServer) commonQueryHandler(proto dnsProtocol, kind string, qtype uint16, - zoneLookup func(ZoneLookup, *dns.Question, *dns.Msg) (*dns.Msg, []ZoneRecord, error), fallback dns.HandlerFunc) dns.HandlerFunc { +// Process a request for a name/address in the '.weave.local.' zone +func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16, + lookup func(inaddr string) ([]ZoneRecord, error), + msgBuilder DNSResponseBuilder, observer ZoneObserverFunc, fallback dns.HandlerFunc) dns.HandlerFunc { return func(w dns.ResponseWriter, r *dns.Msg) { q := r.Question[0] Debug.Printf("[dns msgid %d] %s: %+v", r.MsgHdr.Id, kind, q) maxLen := getMaxReplyLen(r, proto) - lookups := []ZoneLookup{s.Zone, s.mdnsCli} - reply, err := s.cache.Get(r, maxLen) - if err != nil { - if err == errNoLocalReplies { - Debug.Printf("[dns msgid %d] Cached 'no local replies' - skipping local lookup", r.MsgHdr.Id) - lookups = []ZoneLookup{s.Zone} - } else { - Debug.Printf("[dns msgid %d] Error from cache: %s", r.MsgHdr.Id, err) - w.WriteMsg(makeDNSFailResponse(r)) - return + // cache a reponse if the cache is enabled, and observe the name/IP + maybeCache := func(m *dns.Msg, ttl int, flags uint8) { + if !s.cacheDisabled { + s.cache.Put(r, m, ttl, flags) + // any change in the Zone database for this IP/name will lead to this + // cache entry being removed... + // TODO: this closure results in unnecessary `Remove`s and some wasted + // mem... but we can live with that. + observer(q.Name, func() { s.cache.Remove(&q) }) } } - if reply != nil { - Debug.Printf("[dns msgid %d] Returning reply from cache: %s/%d answers", - r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer)) - w.WriteMsg(reply) - return + + if !s.cacheDisabled { + reply, err := s.cache.Get(r, maxLen) + if err != nil { + if err == errNoLocalReplies { + Debug.Printf("[dns msgid %d] Cached no-local-replies", r.MsgHdr.Id) + fallback(w, r) + return + } else { + Debug.Printf("[dns msgid %d] Error from cache: %s", r.MsgHdr.Id, err) + w.WriteMsg(makeDNSFailResponse(r)) + return + } + } + if reply != nil { + Debug.Printf("[dns msgid %d] Returning reply from cache: %s/%d answers", + r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer)) + w.WriteMsg(reply) + return + } } // catch unsupported queries if q.Qtype != qtype { Debug.Printf("[dns msgid %d] Unsupported query type %s", r.MsgHdr.Id, dns.TypeToString[q.Qtype]) m := makeDNSFailResponse(r) - s.cache.Put(r, m, negLocalTTL, 0) + maybeCache(m, s.negLocalTTL, 0) w.WriteMsg(m) return } - for _, lookup := range lookups { - if m, answers, err := zoneLookup(lookup, &q, r); err == nil { - m.Authoritative = true - Debug.Printf("[dns msgid %d] Caching response for type %s query for '%s': %s [code:%s]", - m.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name, answers, dns.RcodeToString[m.Rcode]) - s.cache.Put(r, m, nullTTL, 0) - w.WriteMsg(m) - return - } + if answers, err := lookup(q.Name); err != nil { + Info.Printf("[dns msgid %d] No results for type %s query for '%s' [caching no-local]", + r.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name) + maybeCache(nil, s.negLocalTTL, CacheNoLocalReplies) + fallback(w, r) + } else { + m := msgBuilder(r, &q, answers) + m.Authoritative = true + Debug.Printf("[dns msgid %d] Sending response: %s/%s:%s [code:%s]", + m.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name, answers, dns.RcodeToString[m.Rcode]) + maybeCache(m, nullTTL, 0) + w.WriteMsg(m) } - - Info.Printf("[dns msgid %d] No results for type %s query for '%s' [caching no-local]", - r.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name) - s.cache.Put(r, nil, negLocalTTL, CacheNoLocalReplies) - - fallback(w, r) } } // When we receive a request for a name outside of our '.weave.local.' // domain, ask the configured DNS server as a fallback. func (s *DNSServer) notUsHandler(proto dnsProtocol) dns.HandlerFunc { - dnsClient := proto.GetNewClient(DefaultUDPBuflen) + dnsClient := proto.GetNewClient(DefaultUDPBuflen, s.timeout) return func(w dns.ResponseWriter, r *dns.Msg) { - maxLen := getMaxReplyLen(r, proto) q := r.Question[0] - // create a request where we announce our max payload size + // announce our max payload size as the max payload our client supports + maxLen := getMaxReplyLen(r, proto) rcopy := r rcopy.SetEdns0(uint16(maxLen), false) @@ -336,3 +369,12 @@ func (s *DNSServer) notUsHandler(proto dnsProtocol) dns.HandlerFunc { w.WriteMsg(makeDNSFailResponse(r)) } } + +// Get the listen port +func (s *DNSServer) GetPort() (int, error) { + _, portS, err := net.SplitHostPort(s.ListenAddr) + if err != nil { + return 0, err + } + return strconv.Atoi(portS) +} diff --git a/nameserver/server_test.go b/nameserver/server_test.go index 33ff6cb0..8f1897d1 100644 --- a/nameserver/server_test.go +++ b/nameserver/server_test.go @@ -18,10 +18,6 @@ const ( testUDPBufSize = 16384 ) -var ( - testPort = 17625 -) - func setupForTest(t *testing.T) { // fail early if we cannot find a default multicast interface multicast, err := LinkLocalMulticastListener(nil) @@ -29,18 +25,17 @@ func setupForTest(t *testing.T) { t.Fatalf("Unable to create multicast listener: %s. No default multicast interface?", err) } multicast.Close() - - testPort++ } func TestUDPDNSServer(t *testing.T) { setupForTest(t) + const ( - containerID = "foobar" successTestName = "test1.weave.local." - failTestName = "test2.weave.local." + failTestName = "fail.weave.local." nonLocalName = "weave.works." testAddr1 = "10.2.2.1" + containerID = "somecontainer" ) testCIDR1 := testAddr1 + "/24" @@ -80,15 +75,17 @@ func TestUDPDNSServer(t *testing.T) { } // Run another DNS server for fallback - s, fallbackAddr, err := runLocalUDPServer("127.0.0.1:0", fallbackHandler) + fallback, err := newMockedFallback(fallbackHandler, nil) wt.AssertNoErr(t, err) - defer s.Shutdown() + fallback.Start() + defer fallback.Stop() - _, fallbackPort, err := net.SplitHostPort(fallbackAddr) - wt.AssertNoErr(t, err) - - config := &dns.ClientConfig{Servers: []string{"127.0.0.1"}, Port: fallbackPort} - srv, err := NewDNSServer(DNSServerConfig{UpstreamCfg: config, Port: testPort}, zone, nil) + srv, err := NewDNSServer(DNSServerConfig{ + Zone: zone, + UpstreamCfg: fallback.CliConfig, + CacheDisabled: true, + ListenReadTimeout: testSocketTimeout, + }) wt.AssertNoErr(t, err) defer srv.Stop() go srv.Start() @@ -96,6 +93,10 @@ func TestUDPDNSServer(t *testing.T) { var r *dns.Msg + testPort, err := srv.GetPort() + wt.AssertNoErr(t, err) + wt.AssertNotEqualInt(t, testPort, 0, "invalid listen port") + _, r = assertExchange(t, successTestName, dns.TypeA, testPort, 1, 1, 0) wt.AssertType(t, r.Answer[0], (*dns.A)(nil), "DNS record") wt.AssertEqualString(t, r.Answer[0].(*dns.A).A.String(), testAddr1, "IP address") @@ -126,11 +127,11 @@ func TestUDPDNSServer(t *testing.T) { func TestTCPDNSServer(t *testing.T) { setupForTest(t) + const ( numAnswers = 512 nonLocalName = "weave.works." ) - dnsAddr := fmt.Sprintf("localhost:%d", testPort) InitDefaultLogging(testing.Verbose()) Info.Println("TestTCPDNSServer starting") @@ -140,13 +141,14 @@ func TestTCPDNSServer(t *testing.T) { err = zone.Start() wt.AssertNoErr(t, err) defer zone.Stop() - + // generate a list of `numAnswers` IP addresses var addrs []ZoneRecord bs := make([]byte, 4) for i := 0; i < numAnswers; i++ { binary.LittleEndian.PutUint32(bs, uint32(i)) - addrs = append(addrs, Record{"", net.IPv4(bs[0], bs[1], bs[2], bs[3]), 0, 0, 0}) + ip := net.IPv4(bs[0], bs[1], bs[2], bs[3]) + addrs = append(addrs, ZoneRecord(Record{"", ip, 0, 0, 0})) } // handler for the fallback server: it will just return a very long response @@ -173,27 +175,28 @@ func TestTCPDNSServer(t *testing.T) { } t.Logf("Running a DNS fallback server with UDP") - us, fallbackUDPAddr, err := runLocalUDPServer("127.0.0.1:0", fallbackUDPHandler) + fallback, err := newMockedFallback(fallbackUDPHandler, fallbackTCPHandler) wt.AssertNoErr(t, err) - defer us.Shutdown() + fallback.Start() + defer fallback.Stop() - _, fallbackPort, err := net.SplitHostPort(fallbackUDPAddr) - wt.AssertNoErr(t, err) - - t.Logf("Starting another fallback server, with TCP, on the same port as the UDP server") - fallbackTCPAddr := fmt.Sprintf("127.0.0.1:%s", fallbackPort) - ts, fallbackTCPAddr, err := runLocalTCPServer(fallbackTCPAddr, fallbackTCPHandler) - wt.AssertNoErr(t, err) - defer ts.Shutdown() - - t.Logf("Creating a WeaveDNS server instance, falling back to 127.0.0.1:%s", fallbackPort) - config := &dns.ClientConfig{Servers: []string{"127.0.0.1"}, Port: fallbackPort} - srv, err := NewDNSServer(DNSServerConfig{UpstreamCfg: config, Port: testPort}, zone, nil) + t.Logf("Creating a WeaveDNS server instance, falling back to 127.0.0.1:%s", fallback.Port) + srv, err := NewDNSServer(DNSServerConfig{ + Zone: zone, + UpstreamCfg: fallback.CliConfig, + CacheDisabled: true, + ListenReadTimeout: testSocketTimeout, + }) wt.AssertNoErr(t, err) defer srv.Stop() go srv.Start() time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start + testPort, err := srv.GetPort() + wt.AssertNoErr(t, err) + wt.AssertNotEqualInt(t, testPort, 0, "listen port") + dnsAddr := fmt.Sprintf("127.0.0.1:%d", testPort) + t.Logf("Creating a UDP and a TCP client") uc := new(dns.Client) uc.UDPSize = minUDPSize @@ -205,8 +208,8 @@ func TestTCPDNSServer(t *testing.T) { m.RecursionDesired = true m.SetQuestion(nonLocalName, dns.TypeA) - t.Logf("Checking the fallback server at %s returns a truncated response with UDP", fallbackUDPAddr) - r, _, err := uc.Exchange(m, fallbackUDPAddr) + t.Logf("Checking the fallback server at %s returns a truncated response with UDP", fallback.Addr) + r, _, err := uc.Exchange(m, fallback.Addr) t.Logf("Got response from fallback server (UDP) with %d answers", len(r.Answer)) t.Logf("Response:\n%+v\n", r) wt.AssertNoErr(t, err) diff --git a/weavedns/main.go b/weavedns/main.go index 840a7a09..46d0acf2 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -96,13 +96,15 @@ func main() { } srvConfig := weavedns.DNSServerConfig{ - Port: dnsPort, - CacheLen: cacheLen, - Timeout: timeout, - UDPBufLen: udpbuf, + Zone: zone, + Port: dnsPort, + CacheLen: cacheLen, + Timeout: timeout, + UDPBufLen: udpbuf, + CacheDisabled: cacheDisabled, } - srv, err := weavedns.NewDNSServer(srvConfig, zone, iface) + srv, err := weavedns.NewDNSServer(srvConfig) if err != nil { Error.Fatal("[main] Failed to initialize the WeaveDNS server", err) } From 836d01245953e7d2ba24d81466095342d467d53a Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 18 May 2015 12:02:29 +0200 Subject: [PATCH 09/21] Command line arg for changing the number of background updaters Default number of background updaters raised to 4 --- nameserver/zone.go | 13 ++++++++++--- weavedns/main.go | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/nameserver/zone.go b/nameserver/zone.go index c863ddf0..85adae3c 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -19,8 +19,8 @@ const ( DefaultLocalDomain = "weave.local." // The default name used for the local domain DefaultRefreshInterval = int(localTTL) // Period for background updates with mDNS DefaultRelevantTime = 60 // When to forget info about remote info if nobody asks... + DefaultNumUpdaters = 4 // Default number of background updaters - defaultNumUpdaters = 1 // Number of background updaters defaultRefreshMailbox = 1000 // Number of on-the-fly background queries defaultRemoteIdent = "weave:remote" // Ident used for info obtained from mDNS ) @@ -423,6 +423,8 @@ type ZoneConfig struct { Iface *net.Interface // Refresh interval for local names in the zone database (disabled by default) (in seconds) RefreshInterval int + // Number of background updaters for names + RefreshWorkers int // Forget about remote info if nobody asks for it in this time (in seconds) RelevantTime int // Max pending refresh requests @@ -455,6 +457,7 @@ type zoneDb struct { refreshCloseChan chan bool refreshWg sync.WaitGroup refreshInterval time.Duration + refreshWorkers int } // Create a new zone database @@ -475,6 +478,7 @@ func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { refreshChan: make(chan refreshRequest, maxRefreshRequests), refreshSchedChan: make(chan refreshRequest, maxRefreshRequests), refreshCloseChan: make(chan bool), + refreshWorkers: DefaultNumUpdaters, } // fix the default configuration parameters @@ -490,6 +494,9 @@ func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { if config.RelevantTime > 0 { zone.relevantLimit = time.Duration(config.RelevantTime) * time.Second } + if config.RefreshWorkers > 0 { + zone.refreshWorkers = config.RefreshWorkers + } // Create the mDNS client and server if zone.mdnsCli == nil { @@ -523,8 +530,8 @@ func (zone *zoneDb) Start() (err error) { // Start some name refreshers... if zone.refreshInterval > 0 { - Info.Printf("[zonedb] Starting %d background name updaters", defaultNumUpdaters) - for i := 0; i < defaultNumUpdaters; i++ { + Info.Printf("[zonedb] Starting %d background name updaters", zone.refreshWorkers) + for i := 0; i < zone.refreshWorkers; i++ { zone.refreshWg.Add(1) go zone.updater(i) } diff --git a/weavedns/main.go b/weavedns/main.go index 46d0acf2..2199d204 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -25,6 +25,7 @@ func main() { timeout int udpbuf int refreshInterval int + refreshWorkers int relevantTime int cacheLen int cacheDisabled bool @@ -45,6 +46,7 @@ func main() { flag.BoolVar(&debug, "debug", false, "output debugging info to stderr") // advanced options flag.IntVar(&refreshInterval, "refresh", weavedns.DefaultRefreshInterval, "refresh interval (in secs) for local names (0=disable)") + flag.IntVar(&refreshWorkers, "refresh-workers", weavedns.DefaultNumUpdaters, "default number of background updaters") flag.IntVar(&relevantTime, "relevant", weavedns.DefaultRelevantTime, "life time for info in the absence of queries (in secs)") flag.IntVar(&udpbuf, "udpbuf", weavedns.DefaultUDPBuflen, "UDP buffer length") flag.IntVar(&timeout, "timeout", weavedns.DefaultTimeout, "timeout for resolutions (in millisecs)") @@ -76,6 +78,7 @@ func main() { Domain: domain, Iface: iface, RefreshInterval: refreshInterval, + RefreshWorkers: refreshWorkers, RelevantTime: relevantTime, } zone, err := weavedns.NewZoneDb(zoneConfig) From a159ac44d973b82bcc0ec10a9d9c101d1cc1f355 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 10:47:50 +0200 Subject: [PATCH 10/21] Unit tests for cache invalidations as a result of names updates --- nameserver/server_cache_test.go | 105 ++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 nameserver/server_cache_test.go diff --git a/nameserver/server_cache_test.go b/nameserver/server_cache_test.go new file mode 100644 index 00000000..fe920a0e --- /dev/null +++ b/nameserver/server_cache_test.go @@ -0,0 +1,105 @@ +package nameserver + +import ( + "fmt" + "github.com/benbjohnson/clock" + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" + "net" + "testing" + "time" +) + +// Check if the names updates lead to cache invalidations +func TestServerCacheRefresh(t *testing.T) { + const ( + containerID = "somecontainer" + testName1 = "first.weave.local." + testName2 = "second.weave.local." + refreshInterval = int(localTTL) / 3 + ) + + InitDefaultLogging(testing.Verbose()) + Info.Println("TestServerCacheRefresh starting") + + clk := clock.NewMock() + forwardClock := func(secs int) { + Debug.Printf(">>>>>>> Moving clock forward %d seconds - Time traveling >>>>>>>", secs) + clk.Add(time.Duration(secs) * time.Second) + Debug.Printf("<<<<<<< Time travel finished! We are at %s <<<<<<<", clk.Now()) + } + + Debug.Printf("Creating 2 zone databases") + zoneConfig := ZoneConfig{ + RefreshInterval: refreshInterval, + Clock: clk, + } + dbs := newZoneDbsWithMockedMDns(2, zoneConfig) + dbs.Start() + defer dbs.Stop() + + Debug.Printf("Creating a cache") + cache, err := NewCache(1024, clk) + wt.AssertNoErr(t, err) + + Debug.Printf("Creating a real DNS server for the first zone database and with the cache") + srv, err := NewDNSServer(DNSServerConfig{ + Zone: dbs[0].Zone, + Cache: cache, + Clock: clk, + ListenReadTimeout: testSocketTimeout, + }) + wt.AssertNoErr(t, err) + go srv.Start() + defer srv.Stop() + time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start + + testPort, err := srv.GetPort() + wt.AssertNoErr(t, err) + wt.AssertNotEqualInt(t, testPort, 0, "listen port") + + Debug.Printf("Adding an IPs to %s", testName1) + dbs[1].Zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.1")) + + // Zone database #2 at this point: + // first.weave.local = 10.2.2.1 + + // testName1 and testName2 should have no IPs yet + qName1, _ := assertExchange(t, testName1, dns.TypeA, testPort, 1, 1, 0) + qName2, _ := assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) + assertInCache(t, cache, qName1, "after asking for first name") + assertNotLocalInCache(t, cache, qName2, "after asking for second name") + + forwardClock(refreshInterval / 2) + + Debug.Printf("Adding an IP to %s and to %s", testName1, testName2) + dbs[1].Zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.2")) + dbs[1].Zone.AddRecord(containerID, testName2, net.ParseIP("10.9.9.2")) + + // Zone database #2 at this point: + // first.weave.local = 10.2.2.1 10.2.2.2 + // second.weave.local = 10.9.9.2 + forwardClock(refreshInterval/2 + 2) + + // at this point, the testName1 should have been refreshed + // so it should have two IPs, and the cache entry should have been invalidated + assertNotInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1)) + assertNotLocalInCache(t, cache, qName2, fmt.Sprintf("after asking for %s", testName2)) + + qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 2, 2, 0) + qName2, _ = assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) + assertInCache(t, cache, qName1, fmt.Sprintf("after asking for %s", testName1)) + assertNotLocalInCache(t, cache, qName2, "after asking for a unknown name") + + // delete the IPs, and some time passes by so the cache should be purged... + dbs[1].Zone.DeleteRecord(containerID, net.ParseIP("10.2.2.1")) + dbs[1].Zone.DeleteRecord(containerID, net.ParseIP("10.2.2.2")) + forwardClock(refreshInterval + 1) + + qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) + qName2, _ = assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) + assertNotLocalInCache(t, cache, qName1, "after asking for a unknown name") + assertNotLocalInCache(t, cache, qName2, "after asking for a unknown name") + +} From 5fe0a7fbb2c7ce6c9fc842583d41f78d79b4bf50 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Fri, 1 May 2015 10:53:16 +0200 Subject: [PATCH 11/21] Make Stop() idempotent --- nameserver/server.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/nameserver/server.go b/nameserver/server.go index f2bc6d99..c26bc63b 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -232,14 +232,21 @@ func (s *DNSServer) Status() string { // Perform a graceful shutdown func (s *DNSServer) Stop() error { // Stop the listeners/handlers - if err := s.tcpSrv.Shutdown(); err != nil { - return err + if s.tcpSrv != nil { + if err := s.tcpSrv.Shutdown(); err != nil { + return err + } + s.lst.Close() + s.tcpSrv = nil } - if err := s.udpSrv.Shutdown(); err != nil { - return err + if s.udpSrv != nil { + if err := s.udpSrv.Shutdown(); err != nil { + return err + } + s.pc.Close() + s.udpSrv = nil } s.listenersWg.Wait() - return nil } From b87b9423639dc15975d52dff74bc1a88b1de7ff3 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 27 Apr 2015 13:40:48 +0200 Subject: [PATCH 12/21] Check we can use our mDNS client with our mDNS server --- nameserver/addrs.go | 2 + nameserver/mdns_test.go | 169 ++++++++++++++++++++++++++++++++ nameserver/mocks_test.go | 19 +++- nameserver/server_cache_test.go | 15 +-- nameserver/zone.go | 28 +++--- nameserver/zone_lookup.go | 152 ++++++++++++---------------- nameserver/zone_lookup_test.go | 22 ++--- 7 files changed, 276 insertions(+), 131 deletions(-) create mode 100644 nameserver/mdns_test.go diff --git a/nameserver/addrs.go b/nameserver/addrs.go index 6d6e3abd..bb4ac27c 100644 --- a/nameserver/addrs.go +++ b/nameserver/addrs.go @@ -25,6 +25,8 @@ type ZoneLookup interface { LookupInaddr(inaddr string) ([]ZoneRecord, error) } +type ZoneLookupFunc func(target string) ([]ZoneRecord, error) + type ZoneObservable interface { // Observe anything that affects a particular name in the zone ObserveName(name string, observer ZoneRecordObserver) error diff --git a/nameserver/mdns_test.go b/nameserver/mdns_test.go new file mode 100644 index 00000000..0ed34f93 --- /dev/null +++ b/nameserver/mdns_test.go @@ -0,0 +1,169 @@ +package nameserver + +import ( + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" + "net" + "testing" + "time" +) + +// Check that we can use a regular mDNS server with a regular mDNS client +func TestClientServerSimpleQuery(t *testing.T) { + InitDefaultLogging(testing.Verbose()) + + testRecord1 := Record{"test.weave.local.", net.ParseIP("10.2.2.1"), 0, 0, 0} + testInAddr1 := "1.2.2.10.in-addr.arpa." + + mzone := newMockedZoneWithRecords([]ZoneRecord{testRecord1}) + mdnsServer, err := NewMDNSServer(mzone, true) + wt.AssertNoErr(t, err) + err = mdnsServer.Start(nil) + wt.AssertNoErr(t, err) + defer mdnsServer.Stop() + + var receivedAddr net.IP + var receivedName string + receivedCount := 0 + + mdnsCli, err := NewMDNSClient() + wt.AssertNoErr(t, err) + err = mdnsCli.Start(nil) + wt.AssertNoErr(t, err) + + sendQuery := func(name string, querytype uint16) { + receivedAddr = nil + receivedName = "" + receivedCount = 0 + Debug.Printf("Sending query...") + switch querytype { + case dns.TypeA: + r, err := mdnsCli.LookupName(name) + if len(r) > 0 { + receivedAddr = r[0].IP() + if err == nil { + receivedCount++ + } + } + case dns.TypePTR: + r, err := mdnsCli.LookupInaddr(name) + if len(r) > 0 { + receivedName = r[0].Name() + if err == nil { + receivedCount++ + } + } + } + } + + time.Sleep(100 * time.Millisecond) // Allow for server to get going + + Debug.Printf("Query: %s dns.TypeA", testRecord1.Name()) + sendQuery(testRecord1.Name(), dns.TypeA) + if receivedCount != 1 { + t.Fatalf("Unexpected result count %d for %s", receivedCount, testRecord1.Name()) + } + if !receivedAddr.Equal(testRecord1.IP()) { + t.Fatalf("Unexpected result %s for %s", receivedAddr, testRecord1.Name()) + } + + Debug.Printf("Query: testfail.weave. dns.TypeA") + sendQuery("testfail.weave.", dns.TypeA) + if receivedCount != 0 { + t.Fatalf("Unexpected result count %d for testfail.weave", receivedCount) + } + + Debug.Printf("Query: %s dns.TypePTR", testInAddr1) + sendQuery(testInAddr1, dns.TypePTR) + if receivedCount != 1 { + t.Fatalf("Expected an answer to %s, got %d answers", testInAddr1, receivedCount) + } else if !(testRecord1.Name() == receivedName) { + t.Fatalf("Expected answer %s to query for %s, got %s", testRecord1.Name(), testInAddr1, receivedName) + } +} + +// Check that we can use a use "insistent" queries +func TestClientServerInsistentQuery(t *testing.T) { + InitDefaultLogging(testing.Verbose()) + + testRecord1 := Record{"test.weave.local.", net.ParseIP("10.2.2.1"), 0, 0, 0} + testInAddr1 := "1.2.2.10.in-addr.arpa." + testRecord2 := Record{"test.weave.local.", net.ParseIP("10.2.2.2"), 0, 0, 0} + testInAddr2 := "2.2.2.10.in-addr.arpa." + + mzone1 := newMockedZoneWithRecords([]ZoneRecord{testRecord1}) + mdnsServer1, err := NewMDNSServer(mzone1, true) + wt.AssertNoErr(t, err) + err = mdnsServer1.Start(nil) + wt.AssertNoErr(t, err) + defer mdnsServer1.Stop() + + mzone2 := newMockedZoneWithRecords([]ZoneRecord{testRecord2}) + mdnsServer2, err := NewMDNSServer(mzone2, true) + wt.AssertNoErr(t, err) + err = mdnsServer2.Start(nil) + wt.AssertNoErr(t, err) + defer mdnsServer2.Stop() + + // create a third server with exactly the same info as the second server (so we can test duplicates removals) + mdnsServer3, err := NewMDNSServer(mzone2, true) + wt.AssertNoErr(t, err) + err = mdnsServer3.Start(nil) + wt.AssertNoErr(t, err) + defer mdnsServer3.Stop() + + var receivedAddrs []ZoneRecord + var receivedNames []ZoneRecord + receivedCount := 0 + + mdnsCli, err := NewMDNSClient() + wt.AssertNoErr(t, err) + err = mdnsCli.Start(nil) + wt.AssertNoErr(t, err) + + sendQuery := func(name string, querytype uint16) { + receivedAddrs = nil + receivedNames = nil + receivedCount = 0 + Debug.Printf("Sending query...") + switch querytype { + case dns.TypeA: + receivedAddrs, err = mdnsCli.InsistentLookupName(name) + if err == nil { + receivedCount = len(receivedAddrs) + } + case dns.TypePTR: + receivedNames, err = mdnsCli.InsistentLookupInaddr(name) + if err == nil { + receivedCount = len(receivedNames) + } + } + } + + time.Sleep(100 * time.Millisecond) // Allow for server to get going + + Debug.Printf("Query: %s dns.TypeA", testRecord1.Name()) + sendQuery(testRecord1.Name(), dns.TypeA) + if receivedCount != 2 { + t.Fatalf("Unexpected result count %d for %s", receivedCount, testRecord1.Name()) + } + + Debug.Printf("Query: testfail.weave. dns.TypeA") + sendQuery("testfail.weave.", dns.TypeA) + if receivedCount != 0 { + t.Fatalf("Unexpected result count %d for testfail.weave", receivedCount) + } + + Debug.Printf("Query: %s dns.TypePTR", testInAddr1) + sendQuery(testInAddr1, dns.TypePTR) + if receivedCount != 1 { + t.Fatalf("Expected an answer to %s, got %d answers", testInAddr1, receivedCount) + } + + Debug.Printf("Query: %s dns.TypePTR", testInAddr2) + sendQuery(testInAddr2, dns.TypePTR) + if receivedCount != 1 { + t.Fatalf("Expected an answer to %s, got %d answers", testInAddr2, receivedCount) + } +} diff --git a/nameserver/mocks_test.go b/nameserver/mocks_test.go index 55fabdcb..7cafafd3 100644 --- a/nameserver/mocks_test.go +++ b/nameserver/mocks_test.go @@ -2,6 +2,7 @@ package nameserver import ( "fmt" + "github.com/benbjohnson/clock" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" @@ -423,6 +424,22 @@ func runLocalTCPServer(laddr string, handler dns.HandlerFunc) (*dns.Server, stri ////////////////////////////////////////////////////////////////// +type mockedClock struct { + *clock.Mock +} + +func newMockedClock() *mockedClock { + return &mockedClock{clock.NewMock()} +} + +func (clk *mockedClock) Forward(secs int) { + Debug.Printf(">>>>>>> Moving clock forward %d seconds - Time traveling >>>>>>>", secs) + clk.Add(time.Duration(secs) * time.Second) + Debug.Printf("<<<<<<< Time travel finished! We are at %s <<<<<<<", clk.Now()) +} + +////////////////////////////////////////////////////////////////// + // Perform a DNS query and assert the reply code, number or answers, etc func assertExchange(t *testing.T, z string, ty uint16, port int, minAnswers int, maxAnswers int, expErr int) (*dns.Msg, *dns.Msg) { wt.AssertNotEqualInt(t, port, 0, "invalid DNS server port") @@ -440,7 +457,7 @@ func assertExchange(t *testing.T, z string, ty uint16, port int, minAnswers int, r, _, err := c.Exchange(m, lstAddr) t.Logf("Response from '%s':\n%+v\n", lstAddr, r) if err != nil { - t.Fatalf("Error when querying DNS server at %s: %s", lstAddr, err) + t.Errorf("Error when querying DNS server at %s: %s", lstAddr, err) } wt.AssertNoErr(t, err) if minAnswers == 0 && maxAnswers == 0 { diff --git a/nameserver/server_cache_test.go b/nameserver/server_cache_test.go index fe920a0e..c06eae1c 100644 --- a/nameserver/server_cache_test.go +++ b/nameserver/server_cache_test.go @@ -2,7 +2,6 @@ package nameserver import ( "fmt" - "github.com/benbjohnson/clock" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" @@ -22,13 +21,7 @@ func TestServerCacheRefresh(t *testing.T) { InitDefaultLogging(testing.Verbose()) Info.Println("TestServerCacheRefresh starting") - - clk := clock.NewMock() - forwardClock := func(secs int) { - Debug.Printf(">>>>>>> Moving clock forward %d seconds - Time traveling >>>>>>>", secs) - clk.Add(time.Duration(secs) * time.Second) - Debug.Printf("<<<<<<< Time travel finished! We are at %s <<<<<<<", clk.Now()) - } + clk := newMockedClock() Debug.Printf("Creating 2 zone databases") zoneConfig := ZoneConfig{ @@ -71,7 +64,7 @@ func TestServerCacheRefresh(t *testing.T) { assertInCache(t, cache, qName1, "after asking for first name") assertNotLocalInCache(t, cache, qName2, "after asking for second name") - forwardClock(refreshInterval / 2) + clk.Forward(refreshInterval / 2) Debug.Printf("Adding an IP to %s and to %s", testName1, testName2) dbs[1].Zone.AddRecord(containerID, testName1, net.ParseIP("10.2.2.2")) @@ -80,7 +73,7 @@ func TestServerCacheRefresh(t *testing.T) { // Zone database #2 at this point: // first.weave.local = 10.2.2.1 10.2.2.2 // second.weave.local = 10.9.9.2 - forwardClock(refreshInterval/2 + 2) + clk.Forward(refreshInterval/2 + 2) // at this point, the testName1 should have been refreshed // so it should have two IPs, and the cache entry should have been invalidated @@ -95,7 +88,7 @@ func TestServerCacheRefresh(t *testing.T) { // delete the IPs, and some time passes by so the cache should be purged... dbs[1].Zone.DeleteRecord(containerID, net.ParseIP("10.2.2.1")) dbs[1].Zone.DeleteRecord(containerID, net.ParseIP("10.2.2.2")) - forwardClock(refreshInterval + 1) + clk.Forward(refreshInterval + 1) qName1, _ = assertExchange(t, testName1, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) qName2, _ = assertExchange(t, testName2, dns.TypeA, testPort, 0, 0, dns.RcodeNameError) diff --git a/nameserver/zone.go b/nameserver/zone.go index 85adae3c..6f22ea79 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -122,7 +122,7 @@ func (re *recordEntry) notifyIPObservers() { } } -func (re recordEntry) TTL() int { +func (re *recordEntry) TTL() int { // if we never set the TTL (eg, when using AddRecord()), return the standard value... if re.ttl == 0 { return int(localTTL) @@ -131,11 +131,11 @@ func (re recordEntry) TTL() int { } // Check if the info in this record is still valid according to the TTL -func (re recordEntry) hasExpired(now time.Time) bool { +func (re *recordEntry) hasExpired(now time.Time) bool { return re.ttl > 0 && now.Sub(re.insTime).Seconds() > float64(re.ttl) } -func (re recordEntry) String() string { +func (re *recordEntry) String() string { var buf bytes.Buffer fmt.Fprintf(&buf, "%s", re.Record) lobs := len(re.observers) @@ -163,10 +163,10 @@ func newName(n string) *name { observers: make([]ZoneRecordObserver, 0), } } -func (n name) len() int { return len(n.ipv4) } -func (n name) empty() bool { return n.len() == 0 } -func (n name) hasIP(ip net.IP) bool { return n.hasIPv4(ipToIPv4(ip)) } -func (n name) hasIPv4(ip IPv4) bool { _, found := n.ipv4[ip]; return found } +func (n *name) len() int { return len(n.ipv4) } +func (n *name) empty() bool { return n.len() == 0 } +func (n *name) hasIP(ip net.IP) bool { return n.hasIPv4(ipToIPv4(ip)) } +func (n *name) hasIPv4(ip IPv4) bool { _, found := n.ipv4[ip]; return found } // Get all the entries for this name func (n *name) getAllEntries() (res []*recordEntry) { @@ -257,7 +257,7 @@ func (n *name) notifyNameObservers() { } // Convert a set of IP records to a comma-separated string -func (n name) String() string { +func (n *name) String() string { var buf bytes.Buffer for _, ip := range n.ipv4 { fmt.Fprintf(&buf, "%s, ", ip) @@ -276,10 +276,10 @@ type namesSet struct { names map[string]*name } -func newNamesSet() *namesSet { return &namesSet{names: make(map[string]*name)} } -func (ns namesSet) len() int { return len(ns.names) } -func (ns namesSet) empty() bool { return ns.len() == 0 } -func (ns namesSet) hasName(name string) bool { +func newNamesSet() *namesSet { return &namesSet{names: make(map[string]*name)} } +func (ns *namesSet) len() int { return len(ns.names) } +func (ns *namesSet) empty() bool { return ns.len() == 0 } +func (ns *namesSet) hasName(name string) bool { if n, found := ns.names[name]; !found { return false } else { @@ -365,7 +365,7 @@ func (ns *namesSet) deleteIP(ip IPv4) error { } // Get the name query time -func (ns namesSet) getNameLastAccess(n string) time.Time { +func (ns *namesSet) getNameLastAccess(n string) time.Time { if name, found := ns.names[n]; found { return name.lastAccessTime } @@ -383,7 +383,7 @@ func (ns *namesSet) touchName(name string, now time.Time) { } } -func (ns namesSet) String() string { +func (ns *namesSet) String() string { var buf bytes.Buffer for _, name := range ns.names { fmt.Fprintf(&buf, "%s, ", name) diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go index 247df6bd..2042caf9 100644 --- a/nameserver/zone_lookup.go +++ b/nameserver/zone_lookup.go @@ -6,6 +6,20 @@ import ( "time" ) +func (zone *zoneDb) lookup(target string, lfun func(ns *namesSet) []*recordEntry) (res []ZoneRecord, err error) { + for identName, nameset := range zone.idents { + if identName != defaultRemoteIdent { + for _, ze := range lfun(nameset) { + res = append(res, ze) + } + } + } + if len(res) == 0 { + err = LookupError(target) + } + return +} + // 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) { @@ -15,17 +29,50 @@ func (zone *zoneDb) LookupName(name string) (res []ZoneRecord, err error) { // 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) - for identName, nameset := range zone.idents { - if identName != defaultRemoteIdent { - for _, ze := range nameset.getEntriesForName(name) { - res = append(res, ze) - } - } + 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 len(res) == 0 { - err = LookupError(name) + // 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() + remoteIdent := zone.getNamesSet(defaultRemoteIdent) + for i, answer := range lanswers { + res[i], 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 + } } + zone.mx.Unlock() return } @@ -40,7 +87,7 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) for identName, nameset := range zone.idents { for _, ze := range nameset.getEntriesForName(name) { // filter the entries with expired TTL - // locally introduced entries are nver expired as the always have TTL=0 + // 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) @@ -57,69 +104,16 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) if len(res) > 0 { Debug.Printf("[zonedb] '%s' resolved in local database", name) } else { - // no local results have been obtained in the local database: try with a mDNS query - // (this request is not a background query, so we cannot delay the response) - Debug.Printf("[zonedb] name '%s' not in local database: trying with mDNS", name) - ips, err := zone.mdnsCli.LookupName(name) - if err != nil { - Debug.Printf("[zonedb] mDNS lookup error for '%s': %s", name, 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'", ips, name) - res = make([]ZoneRecord, len(ips)) - zone.mx.Lock() - now = zone.clock.Now() - for i, zr := range ips { - res[i], err = zone.getNamesSet(defaultRemoteIdent).addIPToName(zr, now) - if err != nil { - zone.mx.Unlock() - Warning.Printf("[zonedb] IP [%s] insertion for '%s' failed: %s", ips, name, err) - return nil, err - } - } - zone.mx.Unlock() + res, err = zone.domainLookup(name, zone.mdnsCli.LookupName) } if len(res) > 0 { - // start (if we need to) a background refresh for the name - // note: we do not spend time trying to update names that did not return an initial response... 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 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) - for identName, nameset := range zone.idents { - if identName != defaultRemoteIdent { - for _, ze := range nameset.getEntriesForIP(revIPv4) { - res = append(res, ZoneRecord(ze)) - } - } - } - if len(res) == 0 { - err = LookupError(inaddr) - } - return -} - // 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) @@ -136,7 +130,7 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err for identName, nameset := range zone.idents { for _, ze := range nameset.getEntriesForIP(revIPv4) { // filter the entries with expired TTL - // locally introduced entries are nver expired as the always have TTL=0 + // 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) @@ -153,44 +147,20 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err if len(res) > 0 { Debug.Printf("[zonedb] '%s' resolved in local database", inaddr) } else { - // no local results have been obtained in the local database: try with a mDNS query - Debug.Printf("[zonedb] '%s'(%+v) not in local database... trying with mDNS", inaddr, revIPv4) - names, err := zone.mdnsCli.LookupInaddr(inaddr) - if err != nil { - Debug.Printf("[zonedb] mDNS lookup error for '%s': %s", inaddr, 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'", names, revIPv4) - res = make([]ZoneRecord, len(names)) - zone.mx.Lock() - now = zone.clock.Now() - for i, name := range names { - res[i], err = zone.getNamesSet(defaultRemoteIdent).addIPToName(name, now) - if err != nil { - zone.mx.Unlock() - Warning.Printf("[zonedb] Name '%s' insertion for %s failed: %s", name.Name(), revIPv4, err) - return nil, err - } - } - zone.mx.Unlock() + 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 _, name := range res { - zone.startUpdatingName(name.Name()) + for _, r := range res { + zone.startUpdatingName(r.Name()) } return res, nil } - return nil, LookupError(inaddr) - } ////////////////////////////////////////////////////////////////////////////// diff --git a/nameserver/zone_lookup_test.go b/nameserver/zone_lookup_test.go index 8593086b..6c09ab6b 100644 --- a/nameserver/zone_lookup_test.go +++ b/nameserver/zone_lookup_test.go @@ -1,7 +1,6 @@ package nameserver import ( - "github.com/benbjohnson/clock" . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" "net" @@ -26,12 +25,7 @@ func TestZoneRefresh(t *testing.T) { InitDefaultLogging(testing.Verbose()) Info.Println("TestZoneRefresh starting") - clk := clock.NewMock() - forwardClock := func(secs int) { - Debug.Printf(">>>>>>> Moving clock forward %d seconds - Time traveling >>>>>>>", secs) - clk.Add(time.Duration(secs) * time.Second) - Debug.Printf("<<<<<<< Time travel finished! We are at %s <<<<<<<", clk.Now()) - } + clk := newMockedClock() // Create multiple zone databases, linked through mocked mDNS connections zc := ZoneConfig{ @@ -60,14 +54,14 @@ func TestZoneRefresh(t *testing.T) { t.Logf("Db #1 after the lookup:\n%s", dbs[0].Zone) wt.AssertEqualInt(t, len(res), 1, "lookup result") - forwardClock(refreshInterval / 2) + clk.Forward(refreshInterval / 2) Debug.Printf("A couple of seconds later, we should still have one IP for that name") res, err = dbs[0].Zone.DomainLookupName(name) wt.AssertNoErr(t, err) wt.AssertEqualInt(t, len(res), 1, "lookup result") Debug.Printf("And then we add 2 IPs for that name at ZoneDb 2") - forwardClock(1) + clk.Forward(1) Debug.Printf("Adding 2 IPs to '%s' in Db #2", name) dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr2)) dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr3)) @@ -76,7 +70,7 @@ func TestZoneRefresh(t *testing.T) { dbs[0].Zone.DomainLookupName(name) Debug.Printf("Wait for a while, until a refresh is performed...") - forwardClock(refreshInterval + 1) + clk.Forward(refreshInterval + 1) Debug.Printf("A refresh should have been scheduled now: we should have 3 IPs:") Debug.Printf("the first (local) IP and the others obtained from zone2 with a mDNS query") @@ -87,7 +81,7 @@ func TestZoneRefresh(t *testing.T) { wt.AssertEqualInt(t, len(res), 3, "lookup result length") Debug.Printf("We will not ask for `name` for a while, so it will become irrelevant and will be removed...") - forwardClock(relevantTime + refreshInterval + 1) + clk.Forward(relevantTime + refreshInterval + 1) // the name should be irrelevant now, and all remote info should have been // removed from the zone database @@ -97,17 +91,17 @@ func TestZoneRefresh(t *testing.T) { wt.AssertFalse(t, dbs[0].Zone.HasNameRemoteInfo(name), "remote name info") Debug.Printf("There is no remote info about this name at zone 1: a new IP appears remotely meanwhile...") - forwardClock(1) + clk.Forward(1) Debug.Printf("Adding '%s' to Db #2", name) dbs[1].Zone.AddRecord("someident", name, net.ParseIP(addr4)) Debug.Printf("When we ask about this name again, we get 4 IPs (1 local, 3 remote)") - forwardClock(1) + clk.Forward(1) Debug.Printf("Asking for '%s' again... the first lookup will return only the local results", name) res, err = dbs[0].Zone.DomainLookupName(name) wt.AssertEqualInt(t, len(res), 1, "lookup result length") Debug.Printf("... but a second lookup should return all the results in the network") - forwardClock(1) + clk.Forward(1) res, err = dbs[0].Zone.DomainLookupName(name) Debug.Printf("Got: %s", res) wt.AssertEqualInt(t, len(res), 4, "lookup result length") // TODO: this fails 1% of the runs... !!?? From c4b07fd44eda64b06dc9e67f2e8325edcefe08c9 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Tue, 12 May 2015 18:07:07 +0200 Subject: [PATCH 13/21] Shuffle all the replies, not only the ones from the cache... --- nameserver/cache.go | 19 ------------------- nameserver/dns.go | 19 +++++++++++++++++++ nameserver/server.go | 4 +++- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/nameserver/cache.go b/nameserver/cache.go index 644814f0..7a5cc0c1 100644 --- a/nameserver/cache.go +++ b/nameserver/cache.go @@ -9,7 +9,6 @@ import ( "github.com/miekg/dns" . "github.com/weaveworks/weave/common" "math" - "math/rand" "sync" "time" ) @@ -43,21 +42,6 @@ const ( CacheNoLocalReplies uint8 = 1 << iota // not found in local network (stored in the cache so we skip another local lookup or some time) ) -// shuffleAnswers reorders answers for very basic load balancing -func shuffleAnswers(answers []dns.RR) []dns.RR { - n := len(answers) - if n > 1 { - rand.Seed(time.Now().UTC().UnixNano()) - - for i := 0; i < n; i++ { - r := i + rand.Intn(n-i) - answers[r], answers[i] = answers[i], answers[r] - } - } - - return answers -} - // a cache entry type cacheEntry struct { Status entryStatus // status of the entry @@ -124,9 +108,6 @@ func (e *cacheEntry) getReply(request *dns.Msg, maxLen int, now time.Time) (*dns reply.Rcode = e.reply.Rcode reply.Authoritative = true - // shuffle the values, etc... - reply.Answer = shuffleAnswers(reply.Answer) - return reply, nil } diff --git a/nameserver/dns.go b/nameserver/dns.go index 3b8b8c57..be0a7fd0 100644 --- a/nameserver/dns.go +++ b/nameserver/dns.go @@ -2,6 +2,8 @@ package nameserver import ( "github.com/miekg/dns" + "math/rand" + "time" ) const ( @@ -86,3 +88,20 @@ func getMaxReplyLen(r *dns.Msg, proto dnsProtocol) int { } return maxLen } + + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} + +// shuffleAnswers reorders answers for very basic load balancing +func shuffleAnswers(answers []dns.RR) []dns.RR { + if len(answers) > 1 { + for i := range answers { + j := rand.Intn(i + 1) + answers[i], answers[j] = answers[j], answers[i] + } + } + + return answers +} diff --git a/nameserver/server.go b/nameserver/server.go index c26bc63b..6fd244c9 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -283,7 +283,7 @@ func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16, Debug.Printf("[dns msgid %d] %s: %+v", r.MsgHdr.Id, kind, q) maxLen := getMaxReplyLen(r, proto) - // cache a reponse if the cache is enabled, and observe the name/IP + // cache a response if the cache is enabled, and observe the name/IP maybeCache := func(m *dns.Msg, ttl int, flags uint8) { if !s.cacheDisabled { s.cache.Put(r, m, ttl, flags) @@ -311,6 +311,7 @@ func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16, if reply != nil { Debug.Printf("[dns msgid %d] Returning reply from cache: %s/%d answers", r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer)) + reply.Answer = shuffleAnswers(reply.Answer) w.WriteMsg(reply) return } @@ -333,6 +334,7 @@ func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16, } else { m := msgBuilder(r, &q, answers) m.Authoritative = true + m.Answer = shuffleAnswers(m.Answer) Debug.Printf("[dns msgid %d] Sending response: %s/%s:%s [code:%s]", m.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name, answers, dns.RcodeToString[m.Rcode]) maybeCache(m, nullTTL, 0) From 2d919d0a5f8e70b313f1935c2498361968274fe2 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Wed, 13 May 2015 10:29:11 +0200 Subject: [PATCH 14/21] Remove duplicates in answers --- nameserver/zone_lookup.go | 63 ++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go index 2042caf9..35004400 100644 --- a/nameserver/zone_lookup.go +++ b/nameserver/zone_lookup.go @@ -6,18 +6,52 @@ import ( "time" ) +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) { - res = append(res, ze) + uniq.add(ze) } } } - if len(res) == 0 { - err = LookupError(target) + if len(uniq) == 0 { + return nil, LookupError(target) + } else { + return uniq.toSlice(), nil } - return } // Perform a lookup for a name in the zone @@ -63,17 +97,20 @@ func (zone *zoneDb) domainLookup(target string, lfun ZoneLookupFunc) (res []Zone res = make([]ZoneRecord, len(lanswers)) zone.mx.Lock() now := zone.clock.Now() + uniq := newUniqZoneRecords() remoteIdent := zone.getNamesSet(defaultRemoteIdent) - for i, answer := range lanswers { - res[i], err = remoteIdent.addIPToName(answer, now) + 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 + + return uniq.toSlice(), nil } // Perform a lookup for a name in the zone @@ -84,6 +121,7 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) 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 @@ -92,7 +130,7 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) Debug.Printf("[zonedb] '%s': expired entry '%s' ignored: removing", name, ze) nameset.deleteName(name) } else { - res = append(res, ze) + uniq.add(ze) } } if identName != defaultRemoteIdent { @@ -101,8 +139,9 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) } zone.mx.RUnlock() - if len(res) > 0 { + 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) } @@ -127,6 +166,7 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err 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 @@ -135,7 +175,7 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err Debug.Printf("[zonedb] '%s': expired entry '%s' ignored: removing", revIPv4, ze) nameset.deleteIP(revIPv4) } else { - res = append(res, ZoneRecord(ze)) + uniq.add(ze) if identName != defaultRemoteIdent { nameset.touchName(ze.Name(), now) } @@ -144,8 +184,9 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err } zone.mx.RUnlock() - if len(res) > 0 { + 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) } From 954c5cb3945e424f494b925bbd1d4ad993506c6c Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 18 May 2015 17:25:13 +0200 Subject: [PATCH 15/21] Limit the number of answers we provide to users. --- nameserver/dns.go | 10 ++++++++- nameserver/dns_test.go | 38 +++++++++++++++++++++++++++++++++ nameserver/server.go | 19 +++++++++++------ nameserver/server_cache_test.go | 1 + site/weavedns.md | 10 +++++---- weavedns/main.go | 3 +++ 6 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 nameserver/dns_test.go diff --git a/nameserver/dns.go b/nameserver/dns.go index be0a7fd0..3f919993 100644 --- a/nameserver/dns.go +++ b/nameserver/dns.go @@ -89,7 +89,6 @@ func getMaxReplyLen(r *dns.Msg, proto dnsProtocol) int { return maxLen } - func init() { rand.Seed(time.Now().UTC().UnixNano()) } @@ -105,3 +104,12 @@ func shuffleAnswers(answers []dns.RR) []dns.RR { return answers } + +// only take the first `num` answers +func pruneAnswers(answers []dns.RR, num int) []dns.RR { + if num > 0 && len(answers) > num { + // TODO: we should have some prefer locally-introduced answers, etc... + return answers[:num] + } + return answers +} diff --git a/nameserver/dns_test.go b/nameserver/dns_test.go new file mode 100644 index 00000000..58e53784 --- /dev/null +++ b/nameserver/dns_test.go @@ -0,0 +1,38 @@ +package nameserver + +import ( + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" + "net" + "testing" +) + +// Check that we can prune an answer +func TestPrune(t *testing.T) { + InitDefaultLogging(testing.Verbose()) + Info.Println("TestPrune starting") + + questionMsg := new(dns.Msg) + questionMsg.SetQuestion("name", dns.TypeA) + questionMsg.RecursionDesired = true + question := &questionMsg.Question[0] + records := []ZoneRecord{ + Record{"name", net.ParseIP("10.0.1.1"), 0, 0, 0}, + Record{"name", net.ParseIP("10.0.1.2"), 0, 0, 0}, + Record{"name", net.ParseIP("10.0.1.3"), 0, 0, 0}, + Record{"name", net.ParseIP("10.0.1.4"), 0, 0, 0}, + } + + reply := makeAddressReply(questionMsg, question, records) + reply.Answer[0].Header().Ttl = localTTL + + pruned := pruneAnswers(reply.Answer, 1) + wt.AssertEqualInt(t, len(pruned), 1, "wrong number of answers") + + pruned = pruneAnswers(reply.Answer, 2) + wt.AssertEqualInt(t, len(pruned), 2, "wrong number of answers") + + pruned = pruneAnswers(reply.Answer, 0) + wt.AssertEqualInt(t, len(pruned), len(records), "wrong number of answers") +} diff --git a/nameserver/server.go b/nameserver/server.go index 6fd244c9..79a4e802 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -18,6 +18,7 @@ const ( DefaultUDPBuflen = 4096 // bigger than the default 512 DefaultCacheLen = 8192 // default cache capacity DefaultTimeout = 5 // default timeout for DNS resolutions + DefaultMaxAnswers = 1 // default number of answers provided to users ) type DNSServerConfig struct { @@ -45,6 +46,8 @@ type DNSServerConfig struct { Clock clock.Clock // (Optional) Listening socket read timeout (in milliseconds) ListenReadTimeout int + // Maximum number of answers provided to users + MaxAnswers int } type dnsProtocol uint8 @@ -88,6 +91,7 @@ type DNSServer struct { lst net.Listener cache ZoneCache cacheDisabled bool + maxAnswers int negLocalTTL int timeout time.Duration readTimeout time.Duration @@ -107,6 +111,7 @@ func NewDNSServer(config DNSServerConfig) (s *DNSServer, err error) { timeout: time.Duration(config.Timeout) * time.Millisecond, readTimeout: time.Duration(config.ListenReadTimeout) * time.Millisecond, cacheDisabled: false, + maxAnswers: DefaultMaxAnswers, negLocalTTL: negLocalTTL, clock: config.Clock, } @@ -140,6 +145,9 @@ func NewDNSServer(config DNSServerConfig) (s *DNSServer, err error) { if config.UDPBufLen > 0 { s.udpBuf = config.UDPBufLen } + if config.MaxAnswers > 0 { + s.maxAnswers = config.MaxAnswers + } if config.CacheNegLocalTTL > 0 { s.negLocalTTL = config.CacheNegLocalTTL } @@ -301,17 +309,16 @@ func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16, if err == errNoLocalReplies { Debug.Printf("[dns msgid %d] Cached no-local-replies", r.MsgHdr.Id) fallback(w, r) - return } else { Debug.Printf("[dns msgid %d] Error from cache: %s", r.MsgHdr.Id, err) w.WriteMsg(makeDNSFailResponse(r)) - return } + return } if reply != nil { + reply.Answer = pruneAnswers(shuffleAnswers(reply.Answer), s.maxAnswers) Debug.Printf("[dns msgid %d] Returning reply from cache: %s/%d answers", r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer)) - reply.Answer = shuffleAnswers(reply.Answer) w.WriteMsg(reply) return } @@ -334,10 +341,10 @@ func (s *DNSServer) localHandler(proto dnsProtocol, kind string, qtype uint16, } else { m := msgBuilder(r, &q, answers) m.Authoritative = true - m.Answer = shuffleAnswers(m.Answer) - Debug.Printf("[dns msgid %d] Sending response: %s/%s:%s [code:%s]", - m.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name, answers, dns.RcodeToString[m.Rcode]) maybeCache(m, nullTTL, 0) + m.Answer = pruneAnswers(shuffleAnswers(m.Answer), s.maxAnswers) + Debug.Printf("[dns msgid %d] Sending response: %s/%d answers [code:%s]", + m.MsgHdr.Id, dns.TypeToString[q.Qtype], len(m.Answer), dns.RcodeToString[m.Rcode]) w.WriteMsg(m) } } diff --git a/nameserver/server_cache_test.go b/nameserver/server_cache_test.go index c06eae1c..cd16537a 100644 --- a/nameserver/server_cache_test.go +++ b/nameserver/server_cache_test.go @@ -42,6 +42,7 @@ func TestServerCacheRefresh(t *testing.T) { Cache: cache, Clock: clk, ListenReadTimeout: testSocketTimeout, + MaxAnswers: 4, }) wt.AssertNoErr(t, err) go srv.Start() diff --git a/site/weavedns.md b/site/weavedns.md index db736eac..d13009b7 100644 --- a/site/weavedns.md +++ b/site/weavedns.md @@ -44,10 +44,9 @@ $ docker attach $shell1 ``` Note that it is permissible to register multiple containers with the -same name. weaveDNS picks one address to return when asked for the -name. Since weaveDNS removes any container that dies, this is a simple -way to implement redundancy. In the current implementation it does -not attempt to do load-balancing. +same name: weaveDNS picks one address at random on each request. Since +weaveDNS removes any container that dies, this is a simple way to +implement redundancy. Each weaveDNS container started with `launch-dns` needs to be given its own, unique, IP address, in a subnet that is a) common to all @@ -103,6 +102,9 @@ The inverse operation can be carried out using the `dns-remove` command: $ weave dns-remove 10.2.1.27 $shell2 ``` +When queried about a name with multiple IPs, weaveDNS returns a random +result from the set of IPs available. + ## Hot-swapping service containers If you would like to deploy a new version of a service, keep the old diff --git a/weavedns/main.go b/weavedns/main.go index 2199d204..db38cbcd 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -27,6 +27,7 @@ func main() { refreshInterval int refreshWorkers int relevantTime int + maxAnswers int cacheLen int cacheDisabled bool watch bool @@ -47,6 +48,7 @@ func main() { // advanced options flag.IntVar(&refreshInterval, "refresh", weavedns.DefaultRefreshInterval, "refresh interval (in secs) for local names (0=disable)") flag.IntVar(&refreshWorkers, "refresh-workers", weavedns.DefaultNumUpdaters, "default number of background updaters") + flag.IntVar(&maxAnswers, "max-answers", weavedns.DefaultMaxAnswers, "maximum number of answers returned to clients (0=unlimited)") flag.IntVar(&relevantTime, "relevant", weavedns.DefaultRelevantTime, "life time for info in the absence of queries (in secs)") flag.IntVar(&udpbuf, "udpbuf", weavedns.DefaultUDPBuflen, "UDP buffer length") flag.IntVar(&timeout, "timeout", weavedns.DefaultTimeout, "timeout for resolutions (in millisecs)") @@ -102,6 +104,7 @@ func main() { Zone: zone, Port: dnsPort, CacheLen: cacheLen, + MaxAnswers: maxAnswers, Timeout: timeout, UDPBufLen: udpbuf, CacheDisabled: cacheDisabled, From 6f7e385c74e1c50938fff65a4cd7ec5351962b6f Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Tue, 12 May 2015 18:37:05 +0200 Subject: [PATCH 16/21] Unit tests for multiple IPs through the HTTP API --- nameserver/http_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/nameserver/http_test.go b/nameserver/http_test.go index 8b2b778a..f3cee7a9 100644 --- a/nameserver/http_test.go +++ b/nameserver/http_test.go @@ -24,6 +24,7 @@ func TestHttp(t *testing.T) { testDomain = "weave.local." successTestName = "test1." + testDomain testAddr1 = "10.2.2.1/24" + testAddr2 = "10.2.2.2/24" dockerIP = "9.8.7.6" ) @@ -69,6 +70,31 @@ func TestHttp(t *testing.T) { wt.AssertNoErr(t, err) wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response") + // Adding a new IP for the same name should be OK + addrParts2 := strings.Split(testAddr2, "/") + addrURL2 := fmt.Sprintf("http://localhost:%d/name/%s/%s", port, containerID, addrParts2[0]) + resp, err = genForm("PUT", addrURL2, + url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts2[1]}}) + wt.AssertNoErr(t, err) + wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http success response for second IP") + + // Check that we can get two IPs for that name + ip2, _, _ := net.ParseCIDR(testAddr2) + foundIP, err = zone.LookupName(successTestName) + wt.AssertNoErr(t, err) + if len(foundIP) != 2 { + t.Logf("IPs found: %s", foundIP) + t.Fatalf("Unexpected result length: received %d responses", len(foundIP)) + } + if !(foundIP[0].IP().Equal(ip) || foundIP[0].IP().Equal(ip2)) { + t.Logf("IPs found: %s", foundIP) + t.Fatalf("Unexpected result for %s: received %s, expected %s", successTestName, foundIP, ip) + } + if !(foundIP[1].IP().Equal(ip) || foundIP[1].IP().Equal(ip2)) { + t.Logf("IPs found: %s", foundIP) + t.Fatalf("Unexpected result for %s: received %s, expected %s", successTestName, foundIP, ip) + } + // Delete the address resp, err = genForm("DELETE", addrURL, nil) wt.AssertNoErr(t, err) @@ -84,6 +110,11 @@ func TestHttp(t *testing.T) { wt.AssertNoErr(t, err) wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response") + // Delete the second IP + resp, err = genForm("DELETE", addrURL2, nil) + wt.AssertNoErr(t, err) + wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response") + // Check that the address is gone x, err = zone.LookupName(successTestName) t.Logf("Got %s", x) From 32627eccbf6fb13175e819bb56e9ab1a9fb98689 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Wed, 27 May 2015 13:14:30 +0200 Subject: [PATCH 17/21] Fix the timeout for the fallback server client connection --- nameserver/server.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nameserver/server.go b/nameserver/server.go index 79a4e802..940fe08d 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -17,7 +17,7 @@ const ( DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load DefaultUDPBuflen = 4096 // bigger than the default 512 DefaultCacheLen = 8192 // default cache capacity - DefaultTimeout = 5 // default timeout for DNS resolutions + DefaultTimeout = 5000 // default timeout for DNS resolutions (millisecs) DefaultMaxAnswers = 1 // default number of answers provided to users ) @@ -69,6 +69,7 @@ func (proto dnsProtocol) String() string { // get a new dns.Client for a protocol func (proto dnsProtocol) GetNewClient(bufsize int, timeout time.Duration) *dns.Client { + Debug.Printf("[dns] Creating %s DNS client with %s timeout", proto, timeout) switch proto { case protTCP: return &dns.Client{Net: "tcp", ReadTimeout: timeout} @@ -108,8 +109,8 @@ func NewDNSServer(config DNSServerConfig) (s *DNSServer, err error) { ListenAddr: fmt.Sprintf(":%d", config.Port), listenersWg: new(sync.WaitGroup), - timeout: time.Duration(config.Timeout) * time.Millisecond, - readTimeout: time.Duration(config.ListenReadTimeout) * time.Millisecond, + timeout: DefaultTimeout * time.Millisecond, + readTimeout: DefaultTimeout * time.Millisecond, cacheDisabled: false, maxAnswers: DefaultMaxAnswers, negLocalTTL: negLocalTTL, @@ -142,6 +143,9 @@ func NewDNSServer(config DNSServerConfig) (s *DNSServer, err error) { if config.Timeout > 0 { s.timeout = time.Duration(config.Timeout) * time.Millisecond } + if config.ListenReadTimeout > 0 { + s.readTimeout = time.Duration(config.ListenReadTimeout) * time.Millisecond + } if config.UDPBufLen > 0 { s.udpBuf = config.UDPBufLen } From 40d7b2a87db6ee08a98e76d92c346c9a388b52eb Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Wed, 27 May 2015 14:52:03 +0200 Subject: [PATCH 18/21] Dump the zone database status on "weave status" Fix the smoke test by updating the regexps used for parsing "weave status" output --- nameserver/http.go | 1 + test/500_weave_multi_cidr_test.sh | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nameserver/http.go b/nameserver/http.go index bb37d67a..5b9621a8 100644 --- a/nameserver/http.go +++ b/nameserver/http.go @@ -23,6 +23,7 @@ func ListenHTTP(version string, server *DNSServer, domain string, db Zone, port muxRouter.Methods("GET").Path("/status").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "weave DNS", version) fmt.Fprintln(w, server.Status()) + fmt.Fprintln(w, db.Status()) }) muxRouter.Methods("PUT").Path("/name/{id:.+}/{ip:.+}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/test/500_weave_multi_cidr_test.sh b/test/500_weave_multi_cidr_test.sh index 4e3edf32..3514cfa3 100755 --- a/test/500_weave_multi_cidr_test.sh +++ b/test/500_weave_multi_cidr_test.sh @@ -21,11 +21,11 @@ assert_zone_records() { FQDN=$1; shift # Assert correct number of records exist - assert "weave_on $HOST status | grep \"^$CID\" | wc -l" $# + assert "weave_on $HOST status | grep \"^$CID\" | grep -oE \"\b([0-9]{1,3}\.){3}[0-9]{1,3}\b\" | wc -l" $# # Assert correct records exist for IP; do - assert_raises "weave_on $HOST status | grep \"^$CID $IP $FQDN$\"" + assert_raises "weave_on $HOST status | grep \"$CID\" | grep \"$IP\" | grep \"$FQDN\"" done } From 6d2317d60eb4594b3acf1b1a33833365284553e2 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Thu, 28 May 2015 10:04:17 +0200 Subject: [PATCH 19/21] Silence "golint" errors Silence "go vet" errors Run goimports --- nameserver/addrs_test.go | 5 ++- nameserver/cache.go | 7 +-- nameserver/cache_test.go | 9 ++-- nameserver/dns.go | 3 +- nameserver/dns_test.go | 5 ++- nameserver/http.go | 7 +-- nameserver/http_test.go | 3 +- nameserver/mdns_client.go | 3 +- nameserver/mdns_client_test.go | 7 +-- nameserver/mdns_server.go | 3 +- nameserver/mdns_server_test.go | 7 +-- nameserver/mdns_test.go | 7 +-- nameserver/mocks_test.go | 29 +++++++------ nameserver/server.go | 7 +-- nameserver/server_cache_test.go | 7 +-- nameserver/server_test.go | 9 ++-- nameserver/zone.go | 75 +++++++++++++++++---------------- nameserver/zone_lookup.go | 24 +++++------ nameserver/zone_lookup_test.go | 5 ++- nameserver/zone_test.go | 5 ++- weavedns/main.go | 5 ++- 21 files changed, 126 insertions(+), 106 deletions(-) diff --git a/nameserver/addrs_test.go b/nameserver/addrs_test.go index 01c9acc3..1c09d73a 100644 --- a/nameserver/addrs_test.go +++ b/nameserver/addrs_test.go @@ -1,10 +1,11 @@ package nameserver import ( - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" + + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) func TestAddrs(t *testing.T) { diff --git a/nameserver/cache.go b/nameserver/cache.go index 7a5cc0c1..624236b8 100644 --- a/nameserver/cache.go +++ b/nameserver/cache.go @@ -5,12 +5,13 @@ import ( "container/heap" "errors" "fmt" - "github.com/benbjohnson/clock" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" "math" "sync" "time" + + "github.com/benbjohnson/clock" + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" ) var ( diff --git a/nameserver/cache_test.go b/nameserver/cache_test.go index b53d7529..890bea9b 100644 --- a/nameserver/cache_test.go +++ b/nameserver/cache_test.go @@ -2,13 +2,14 @@ package nameserver import ( "fmt" + "net" + "testing" + "time" + "github.com/benbjohnson/clock" "github.com/miekg/dns" . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" - "net" - "testing" - "time" ) // Check that the cache keeps its intended capacity constant @@ -201,7 +202,7 @@ func TestCacheEntries(t *testing.T) { wt.AssertNotNil(t, err, "Get() error with CacheNoLocalReplies") clk.Add(time.Second * time.Duration(negLocalTTL+1)) - t.Logf("Checking that we get an expired response after %f seconds", negLocalTTL) + t.Logf("Checking that we get an expired response after %d seconds", negLocalTTL) resp, err = l.Get(questionMsg3, minUDPSize) wt.AssertNil(t, resp, "expired Get() response with CacheNoLocalReplies") wt.AssertNil(t, err, "expired Get() error with CacheNoLocalReplies") diff --git a/nameserver/dns.go b/nameserver/dns.go index 3f919993..8c08a2db 100644 --- a/nameserver/dns.go +++ b/nameserver/dns.go @@ -1,9 +1,10 @@ package nameserver import ( - "github.com/miekg/dns" "math/rand" "time" + + "github.com/miekg/dns" ) const ( diff --git a/nameserver/dns_test.go b/nameserver/dns_test.go index 58e53784..66eba2cb 100644 --- a/nameserver/dns_test.go +++ b/nameserver/dns_test.go @@ -1,11 +1,12 @@ package nameserver import ( + "net" + "testing" + "github.com/miekg/dns" . "github.com/weaveworks/weave/common" wt "github.com/weaveworks/weave/testing" - "net" - "testing" ) // Check that we can prune an answer diff --git a/nameserver/http.go b/nameserver/http.go index 5b9621a8..1f9a1e2b 100644 --- a/nameserver/http.go +++ b/nameserver/http.go @@ -2,12 +2,13 @@ package nameserver import ( "fmt" - "github.com/gorilla/mux" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" "log" "net" "net/http" + + "github.com/gorilla/mux" + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" ) func httpErrorAndLog(level *log.Logger, w http.ResponseWriter, msg string, diff --git a/nameserver/http_test.go b/nameserver/http_test.go index f3cee7a9..b0175fe7 100644 --- a/nameserver/http_test.go +++ b/nameserver/http_test.go @@ -2,7 +2,6 @@ package nameserver import ( "fmt" - wt "github.com/weaveworks/weave/testing" "math/rand" "net" "net/http" @@ -10,6 +9,8 @@ import ( "strings" "testing" "time" + + wt "github.com/weaveworks/weave/testing" ) func genForm(method string, url string, data url.Values) (resp *http.Response, err error) { diff --git a/nameserver/mdns_client.go b/nameserver/mdns_client.go index 4163e342..2ab57c99 100644 --- a/nameserver/mdns_client.go +++ b/nameserver/mdns_client.go @@ -3,10 +3,11 @@ package nameserver import ( "bytes" "fmt" - "github.com/miekg/dns" "math" "net" "time" + + "github.com/miekg/dns" ) // Portions of this code taken from github.com/armon/mdns diff --git a/nameserver/mdns_client_test.go b/nameserver/mdns_client_test.go index 7880c2a4..89d23757 100644 --- a/nameserver/mdns_client_test.go +++ b/nameserver/mdns_client_test.go @@ -1,13 +1,14 @@ package nameserver import ( - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "log" "net" "testing" "time" + + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) var ( diff --git a/nameserver/mdns_server.go b/nameserver/mdns_server.go index d67318c9..482d9244 100644 --- a/nameserver/mdns_server.go +++ b/nameserver/mdns_server.go @@ -1,9 +1,10 @@ package nameserver import ( + "net" + "github.com/miekg/dns" . "github.com/weaveworks/weave/common" - "net" ) type MDNSServer struct { diff --git a/nameserver/mdns_server_test.go b/nameserver/mdns_server_test.go index 5b11e60e..35ece389 100644 --- a/nameserver/mdns_server_test.go +++ b/nameserver/mdns_server_test.go @@ -1,12 +1,13 @@ package nameserver import ( - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" "time" + + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) func TestServerSimpleQuery(t *testing.T) { diff --git a/nameserver/mdns_test.go b/nameserver/mdns_test.go index 0ed34f93..2baeb3ff 100644 --- a/nameserver/mdns_test.go +++ b/nameserver/mdns_test.go @@ -1,12 +1,13 @@ package nameserver import ( - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" "time" + + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) // Check that we can use a regular mDNS server with a regular mDNS client diff --git a/nameserver/mocks_test.go b/nameserver/mocks_test.go index 7cafafd3..7cf195b4 100644 --- a/nameserver/mocks_test.go +++ b/nameserver/mocks_test.go @@ -2,15 +2,16 @@ package nameserver import ( "fmt" - "github.com/benbjohnson/clock" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "strconv" "sync" "testing" "time" + + "github.com/benbjohnson/clock" + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) const ( @@ -153,7 +154,7 @@ func (mc *mockedMDNSClient) LookupName(name string) ([]ZoneRecord, error) { defer mc.Unlock() // get a random result - mc.NumLookupsName += 1 + mc.NumLookupsName++ for _, server := range mc.servers { r, err := server.Zone().LookupName(name) if err == nil { @@ -168,7 +169,7 @@ func (mc *mockedMDNSClient) LookupInaddr(inaddr string) ([]ZoneRecord, error) { defer mc.Unlock() // get a random result - mc.NumLookupsInaddr += 1 + mc.NumLookupsInaddr++ for _, server := range mc.servers { r, err := server.Zone().LookupInaddr(inaddr) if err == nil { @@ -182,8 +183,8 @@ func (mc *mockedMDNSClient) InsistentLookupName(name string) ([]ZoneRecord, erro mc.Lock() defer mc.Unlock() - res := make([]ZoneRecord, 0) - mc.NumInsLookupsName += 1 + var res []ZoneRecord + mc.NumInsLookupsName++ for _, server := range mc.servers { r, err := server.Zone().LookupName(name) if err == nil { @@ -200,8 +201,8 @@ func (mc *mockedMDNSClient) InsistentLookupInaddr(inaddr string) ([]ZoneRecord, mc.Lock() defer mc.Unlock() - res := make([]ZoneRecord, 0) - mc.NumInsLookupsInaddr += 1 + var res []ZoneRecord + mc.NumInsLookupsInaddr++ for _, server := range mc.servers { r, err := server.Zone().LookupInaddr(inaddr) if err == nil { @@ -231,7 +232,7 @@ func (mc *mockedMDNSClient) RemoveServer(srv ZoneMDNSServer) { type zbmEntry struct { Server *mockedMDNSServer Client *mockedMDNSClient - Zone *zoneDb + Zone *ZoneDb } type zoneDbsWithMockedMDns []*zbmEntry @@ -303,14 +304,14 @@ type mockedCache struct { func newMockedCache() *mockedCache { return &mockedCache{} } func (c *mockedCache) Get(request *dns.Msg, maxLen int) (reply *dns.Msg, err error) { - c.NumGets += 1 + c.NumGets++ return nil, nil } func (c *mockedCache) Put(request *dns.Msg, reply *dns.Msg, ttl int, flags uint8) int { - c.NumPuts += 1 + c.NumPuts++ return 0 } -func (c *mockedCache) Remove(question *dns.Question) { c.NumRemovals += 1 } +func (c *mockedCache) Remove(question *dns.Question) { c.NumRemovals++ } func (c *mockedCache) Purge() {} func (c *mockedCache) Clear() {} func (c *mockedCache) Len() int { return 0 } diff --git a/nameserver/server.go b/nameserver/server.go index 940fe08d..224568ae 100644 --- a/nameserver/server.go +++ b/nameserver/server.go @@ -3,13 +3,14 @@ package nameserver import ( "bytes" "fmt" - "github.com/benbjohnson/clock" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" "net" "strconv" "sync" "time" + + "github.com/benbjohnson/clock" + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" ) const ( diff --git a/nameserver/server_cache_test.go b/nameserver/server_cache_test.go index cd16537a..3e3be2d9 100644 --- a/nameserver/server_cache_test.go +++ b/nameserver/server_cache_test.go @@ -2,12 +2,13 @@ package nameserver import ( "fmt" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" "time" + + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) // Check if the names updates lead to cache invalidations diff --git a/nameserver/server_test.go b/nameserver/server_test.go index 8f1897d1..6330a52e 100644 --- a/nameserver/server_test.go +++ b/nameserver/server_test.go @@ -3,12 +3,13 @@ package nameserver import ( "encoding/binary" "fmt" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" "time" + + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) const ( @@ -180,7 +181,7 @@ func TestTCPDNSServer(t *testing.T) { fallback.Start() defer fallback.Stop() - t.Logf("Creating a WeaveDNS server instance, falling back to 127.0.0.1:%s", fallback.Port) + t.Logf("Creating a WeaveDNS server instance, falling back to 127.0.0.1:%d", fallback.Port) srv, err := NewDNSServer(DNSServerConfig{ Zone: zone, UpstreamCfg: fallback.CliConfig, diff --git a/nameserver/zone.go b/nameserver/zone.go index 6f22ea79..908aee6d 100644 --- a/nameserver/zone.go +++ b/nameserver/zone.go @@ -3,12 +3,13 @@ package nameserver import ( "bytes" "fmt" - "github.com/benbjohnson/clock" - "github.com/miekg/dns" - . "github.com/weaveworks/weave/common" "net" "sync" "time" + + "github.com/benbjohnson/clock" + "github.com/miekg/dns" + . "github.com/weaveworks/weave/common" ) const ( @@ -103,8 +104,8 @@ func newRecordEntry(zr ZoneRecord, now time.Time) *recordEntry { } } -func (i *recordEntry) addIPObserver(zro ZoneRecordObserver) { - i.observers = append(i.observers, zro) +func (re *recordEntry) addIPObserver(zro ZoneRecordObserver) { + re.observers = append(re.observers, zro) } // Notify all the IP observers and then remove them all @@ -280,24 +281,24 @@ func newNamesSet() *namesSet { return &namesSet{names: make(map[string]*name func (ns *namesSet) len() int { return len(ns.names) } func (ns *namesSet) empty() bool { return ns.len() == 0 } func (ns *namesSet) hasName(name string) bool { - if n, found := ns.names[name]; !found { + n, found := ns.names[name] + if !found { return false - } else { - // the name must have some valid IPs in order to be considered as "existent" - return len(n.ipv4) > 0 } + // the name must have some valid IPs in order to be considered as "existent" + return len(n.ipv4) > 0 } // Get the zone entries for a name // If the name does not exist and `create` is true, a new name will be created func (ns *namesSet) getName(name string, create bool) (n *name) { - if n, found := ns.names[name]; !found && create { + n, found := ns.names[name] + if !found && create { newName := newName(name) ns.names[name] = newName return newName - } else { - return n } + return n } // Get all the entries for a name @@ -333,16 +334,16 @@ func (ns *namesSet) addIPToName(zr ZoneRecord, now time.Time) (*recordEntry, err // Delete a name in the names set func (ns *namesSet) deleteName(n string) error { - if name, found := ns.names[n]; found { + name, found := ns.names[n] + if found { for _, ipRecord := range name.getAllEntries() { ipRecord.notifyIPObservers() } name.notifyNameObservers() delete(ns.names, n) return nil - } else { - return LookupError(fmt.Sprintf("%+v", name)) } + return LookupError(fmt.Sprintf("%+v", name)) } // Delete an IPv4 from all names @@ -441,7 +442,7 @@ type ZoneConfig struct { // The zone database contains the locally known information about the zone. // The zone database uses some other mechanisms (eg, mDNS) for finding unknown // information or for keeping current information up to date. -type zoneDb struct { +type ZoneDb struct { mx sync.RWMutex idents identRecordsSet @@ -461,13 +462,13 @@ type zoneDb struct { } // Create a new zone database -func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { +func NewZoneDb(config ZoneConfig) (zone *ZoneDb, err error) { maxRefreshRequests := defaultRefreshMailbox if config.MaxRefreshRequests > 0 { maxRefreshRequests = config.MaxRefreshRequests } - zone = &zoneDb{ + zone = &ZoneDb{ domain: config.Domain, idents: make(identRecordsSet), mdnsCli: config.MDNSClient, @@ -514,7 +515,7 @@ func NewZoneDb(config ZoneConfig) (zone *zoneDb, err error) { } // Start the zone database -func (zone *zoneDb) Start() (err error) { +func (zone *ZoneDb) Start() (err error) { if zone.iface != nil { Info.Printf("[zonedb] Using mDNS on %+v", zone.iface) } else { @@ -548,7 +549,7 @@ func (zone *zoneDb) Start() (err error) { } // Perform a graceful shutdown of the zone database -func (zone *zoneDb) Stop() error { +func (zone *ZoneDb) Stop() error { if zone.refreshInterval > 0 { Debug.Printf("[zonedb] Closing background updaters...") close(zone.refreshCloseChan) @@ -562,12 +563,12 @@ func (zone *zoneDb) Stop() error { } // Obtain the domain where this database keeps information for -func (zone *zoneDb) Domain() string { +func (zone *ZoneDb) Domain() string { return zone.domain } // Return the status string -func (zone *zoneDb) Status() string { +func (zone *ZoneDb) Status() string { var buf bytes.Buffer fmt.Fprintln(&buf, "Local domain", zone.domain) fmt.Fprintln(&buf, "Interface", zone.iface) @@ -576,7 +577,7 @@ func (zone *zoneDb) Status() string { } // Add a record with `name` pointing to `ip` -func (zone *zoneDb) AddRecord(ident string, name string, ip net.IP) (err error) { +func (zone *ZoneDb) AddRecord(ident string, name string, ip net.IP) (err error) { zone.mx.Lock() defer zone.mx.Unlock() Debug.Printf("[zonedb] Adding record: '%s'/'%s'[%s]", ident, name, ip) @@ -586,7 +587,7 @@ func (zone *zoneDb) AddRecord(ident string, name string, ip net.IP) (err error) } // Delete all records for an ident -func (zone *zoneDb) DeleteRecordsFor(ident string) (err error) { +func (zone *ZoneDb) DeleteRecordsFor(ident string) (err error) { zone.mx.Lock() defer zone.mx.Unlock() Debug.Printf("[zonedb] Deleting records for ident '%s'", ident) @@ -595,7 +596,7 @@ func (zone *zoneDb) DeleteRecordsFor(ident string) (err error) { } // Delete an IP in a ident -func (zone *zoneDb) DeleteRecord(ident string, ip net.IP) (err error) { +func (zone *ZoneDb) DeleteRecord(ident string, ip net.IP) (err error) { zone.mx.Lock() defer zone.mx.Unlock() @@ -620,7 +621,7 @@ func (zone *zoneDb) DeleteRecord(ident string, ip net.IP) (err error) { // The observer will be invoked on any change/removal that affects this name. // Each observer will be invoked at least once (but possibly more). After that, they will be removed. // The observer should not try to lock the ZoneDB (you will get a deadlock) -func (zone *zoneDb) ObserveName(name string, observer ZoneRecordObserver) (err error) { +func (zone *ZoneDb) ObserveName(name string, observer ZoneRecordObserver) (err error) { zone.mx.Lock() defer zone.mx.Unlock() @@ -629,7 +630,7 @@ func (zone *zoneDb) ObserveName(name string, observer ZoneRecordObserver) (err e for _, nameset := range zone.idents { if n := nameset.getName(name, false); n != nil { n.addNameObserver(observer) - cnt += 1 + cnt++ } } if cnt == 0 { @@ -646,7 +647,7 @@ func (zone *zoneDb) ObserveName(name string, observer ZoneRecordObserver) (err e // The observer will be invoked on any change/removal that affects this IP. // Each observer will be invoked at least once (but possibly more). After that, they will be removed. // The observer should not try to lock the ZoneDB (you will get a deadlock) -func (zone *zoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (err error) { +func (zone *ZoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (err error) { zone.mx.Lock() defer zone.mx.Unlock() @@ -658,7 +659,7 @@ func (zone *zoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (e for _, nameset := range zone.idents { for _, entry := range nameset.getEntriesForIP(revIP) { entry.addIPObserver(observer) - cnt += 1 + cnt++ } } if cnt == 0 { @@ -668,20 +669,20 @@ func (zone *zoneDb) ObserveInaddr(inaddr string, observer ZoneRecordObserver) (e } // Get the string representation of a zone -func (zone *zoneDb) String() string { +func (zone *ZoneDb) String() string { zone.mx.RLock() defer zone.mx.RUnlock() return zone.idents.String() } // Notify that a container has died -func (zone *zoneDb) ContainerDied(ident string) error { +func (zone *ZoneDb) ContainerDied(ident string) error { Info.Printf("[zonedb] Container %s down. Removing records", ident) return zone.DeleteRecordsFor(ident) } // Return true if a remote name is still relevant -func (zone *zoneDb) IsNameRelevant(n string) bool { +func (zone *ZoneDb) IsNameRelevant(n string) bool { zone.mx.Lock() defer zone.mx.Unlock() @@ -699,7 +700,7 @@ func (zone *zoneDb) IsNameRelevant(n string) bool { // Returns true if the info we have for a remote name has expired. Local names are never expired // Returns false if we do not have info for this name from remote peers. -func (zone *zoneDb) IsNameExpired(n string) bool { +func (zone *ZoneDb) IsNameExpired(n string) bool { zone.mx.Lock() defer zone.mx.Unlock() @@ -716,7 +717,7 @@ func (zone *zoneDb) IsNameExpired(n string) bool { return false } -func (zone *zoneDb) HasNameLocalInfo(n string) bool { +func (zone *ZoneDb) HasNameLocalInfo(n string) bool { zone.mx.Lock() defer zone.mx.Unlock() @@ -734,7 +735,7 @@ func (zone *zoneDb) HasNameLocalInfo(n string) bool { // Return true if the we have obtained some information for a name from remote peers // We can have both local (eg, introduced through the HTTP API) and remote (eg, mDNS) // information for a name. This method only checks if there is remote information... -func (zone *zoneDb) HasNameRemoteInfo(n string) bool { +func (zone *ZoneDb) HasNameRemoteInfo(n string) bool { zone.mx.Lock() defer zone.mx.Unlock() @@ -746,7 +747,7 @@ func (zone *zoneDb) HasNameRemoteInfo(n string) bool { } // Get or create a names set -func (zone *zoneDb) getNamesSet(ident string) *namesSet { +func (zone *ZoneDb) getNamesSet(ident string) *namesSet { ns, found := zone.idents[ident] if !found { ns = newNamesSet() @@ -756,7 +757,7 @@ func (zone *zoneDb) getNamesSet(ident string) *namesSet { } // Delete a ident -func (zone *zoneDb) deleteIdent(ident string) { +func (zone *ZoneDb) deleteIdent(ident string) { Info.Printf("[zonedb] Removing everything for ident '%s'", ident) if ns, found := zone.idents[ident]; found { for _, n := range ns.names { diff --git a/nameserver/zone_lookup.go b/nameserver/zone_lookup.go index 35004400..292648a9 100644 --- a/nameserver/zone_lookup.go +++ b/nameserver/zone_lookup.go @@ -1,9 +1,10 @@ package nameserver import ( + "time" + "github.com/miekg/dns" . "github.com/weaveworks/weave/common" - "time" ) type uniqZoneRecordKey struct { @@ -38,7 +39,7 @@ func (uzr *uniqZoneRecords) toSlice() []ZoneRecord { ////////////////////////////////////////////////////////////////////////////// // Lookup in the database for locally-introduced information -func (zone *zoneDb) lookup(target string, lfun func(ns *namesSet) []*recordEntry) (res []ZoneRecord, err error) { +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 { @@ -49,14 +50,13 @@ func (zone *zoneDb) lookup(target string, lfun func(ns *namesSet) []*recordEntry } if len(uniq) == 0 { return nil, LookupError(target) - } else { - return uniq.toSlice(), nil } + 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) { +func (zone *ZoneDb) LookupName(name string) (res []ZoneRecord, err error) { zone.mx.RLock() defer zone.mx.RUnlock() @@ -68,7 +68,7 @@ func (zone *zoneDb) LookupName(name string) (res []ZoneRecord, err error) { // 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) { +func (zone *ZoneDb) LookupInaddr(inaddr string) (res []ZoneRecord, err error) { zone.mx.RLock() defer zone.mx.RUnlock() @@ -82,7 +82,7 @@ func (zone *zoneDb) LookupInaddr(inaddr string) (res []ZoneRecord, err error) { } // Perform a domain lookup with mDNS -func (zone *zoneDb) domainLookup(target string, lfun ZoneLookupFunc) (res []ZoneRecord, err error) { +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) @@ -115,7 +115,7 @@ func (zone *zoneDb) domainLookup(target string, lfun ZoneLookupFunc) (res []Zone // 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) { +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) @@ -156,7 +156,7 @@ func (zone *zoneDb) DomainLookupName(name string) (res []ZoneRecord, err error) // 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) { +func (zone *ZoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err error) { revIPv4, err := raddrToIPv4(inaddr) if err != nil { return nil, newParseError("lookup address", inaddr) @@ -239,7 +239,7 @@ func (zone *zoneDb) DomainLookupInaddr(inaddr string) (res []ZoneRecord, err err // 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) { +func (zone *ZoneDb) startUpdatingName(name string) { if zone.refreshInterval > 0 { zone.mx.Lock() defer zone.mx.Unlock() @@ -257,7 +257,7 @@ func (zone *zoneDb) startUpdatingName(name string) { } // A worker for updating the list of IPs we have for a name -func (zone *zoneDb) updater(num int) { +func (zone *ZoneDb) updater(num int) { defer zone.refreshWg.Done() Debug.Printf("[zonedb] Starting background updater #%d...", num) @@ -310,7 +310,7 @@ func (zone *zoneDb) updater(num int) { // The periodic updater // Consume requests from the `refreshSchedChan`, where requests with increasing scheduling time are enqueued // for refreshing names... -func (zone *zoneDb) periodicUpdater() { +func (zone *ZoneDb) periodicUpdater() { defer zone.refreshWg.Done() for { select { diff --git a/nameserver/zone_lookup_test.go b/nameserver/zone_lookup_test.go index 6c09ab6b..fde104bb 100644 --- a/nameserver/zone_lookup_test.go +++ b/nameserver/zone_lookup_test.go @@ -1,11 +1,12 @@ package nameserver import ( - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" "time" + + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) // Check that the refreshing mechanism works as expected diff --git a/nameserver/zone_test.go b/nameserver/zone_test.go index 968b4b7d..63d98fb3 100644 --- a/nameserver/zone_test.go +++ b/nameserver/zone_test.go @@ -2,10 +2,11 @@ package nameserver import ( "fmt" - . "github.com/weaveworks/weave/common" - wt "github.com/weaveworks/weave/testing" "net" "testing" + + . "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" ) func TestZone(t *testing.T) { diff --git a/weavedns/main.go b/weavedns/main.go index db38cbcd..93a7bfd0 100644 --- a/weavedns/main.go +++ b/weavedns/main.go @@ -3,12 +3,13 @@ package main import ( "flag" "fmt" + "net" + "os" + . "github.com/weaveworks/weave/common" "github.com/weaveworks/weave/common/updater" weavedns "github.com/weaveworks/weave/nameserver" weavenet "github.com/weaveworks/weave/net" - "net" - "os" ) var version = "(unreleased version)" From b8d4068a264f216f68507a46e37f93bae86262db Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Wed, 27 May 2015 13:15:14 +0200 Subject: [PATCH 20/21] Force 8.8.8.8 as the DNS server in VirtualBoxes --- test/Vagrantfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/Vagrantfile b/test/Vagrantfile index 5e0a4850..57ef1097 100644 --- a/test/Vagrantfile +++ b/test/Vagrantfile @@ -34,6 +34,11 @@ def configure_docker(host, hostname, ip) cleanup(host.vm) end +def configure_resolv_conf(host) + # Fix the resolution errors by using 8.8.8.8 + host.vm.provision :shell, :inline => "echo 'nameserver 8.8.8.8' > /etc/resolv.conf" +end + Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| @@ -48,6 +53,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| (1..n_machines).each do |i| config.vm.define "host#{i}" do |host| ip_suffix = ip_suffix_base + i + configure_resolv_conf(host) configure_docker(host, "host#{i}", "#{ip_prefix}.#{ip_suffix}") end end From 0a803c61ffd23cb11931884695fbfef304530ce5 Mon Sep 17 00:00:00 2001 From: Alvaro Saurin Date: Mon, 18 May 2015 16:17:51 +0200 Subject: [PATCH 21/21] Smoke tests --- test/210_dns_multicast_test.sh | 14 ++++++++++---- test/240_dns_add_name_test.sh | 4 ++-- test/290_dns_fallback_test.sh | 2 +- test/config.sh | 28 +++++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/test/210_dns_multicast_test.sh b/test/210_dns_multicast_test.sh index 097ef027..f16d2eeb 100755 --- a/test/210_dns_multicast_test.sh +++ b/test/210_dns_multicast_test.sh @@ -3,7 +3,9 @@ . ./config.sh C1=10.2.3.78 -C2=10.2.3.34 +C2_1=10.2.3.34 +C2_2=10.2.3.35 +C2_3=10.2.3.36 UNIVERSE=10.2.4.0/24 NAME2=seetwo.weave.local NAME4=seefour.weave.local @@ -16,13 +18,17 @@ weave_on $HOST2 launch -iprange $UNIVERSE $HOST1 weave_on $HOST1 launch-dns 10.2.254.1/24 weave_on $HOST2 launch-dns 10.2.254.2/24 -start_container $HOST2 $C2/24 --name=c2 -h $NAME2 -start_container $HOST2 --name=c4 -h $NAME4 +start_container $HOST2 $C2_1/24 --name=c2 -h $NAME2 +start_container $HOST2 --name=c4 -h $NAME4 + +weave_on $HOST2 dns-add $C2_2 c2 -h $NAME2 +weave_on $HOST2 dns-add $C2_3 c2 -h $NAME2 + start_container_with_dns $HOST1 $C1/24 --name=c1 start_container_with_dns $HOST1 --name=c3 C4=$(container_ip $HOST2 c4) -assert_dns_record $HOST1 c1 $NAME2 $C2 +assert_dns_record $HOST1 c1 $NAME2 $C2_1 $C2_2 $C2_3 assert_dns_record $HOST1 c3 $NAME4 $C4 assert_raises "exec_on $HOST1 c1 getent hosts 8.8.8.8 | grep google" diff --git a/test/240_dns_add_name_test.sh b/test/240_dns_add_name_test.sh index 0377cb11..59da781a 100755 --- a/test/240_dns_add_name_test.sh +++ b/test/240_dns_add_name_test.sh @@ -9,7 +9,7 @@ NAME2=seetwo.weave.local start_suite "Add and remove names on a single host" -weave_on $HOST1 launch-dns 10.2.254.1/24 +weave_on $HOST1 launch-dns 10.2.254.1/24 --no-cache start_container $HOST1 $C2/24 --name=c2 start_container_with_dns $HOST1 $C1/24 --name=c1 @@ -24,6 +24,6 @@ assert "exec_on $HOST1 c1 getent hosts $C1 | tr -s ' '" "$C1 $NAME1" weave_on $HOST1 dns-remove $C1 c1 -assert_raises "exec_on $HOST1 c1 getent hosts $NAME1" 2 +assert_no_dns_record $HOST1 c1 $NAME1 end_suite diff --git a/test/290_dns_fallback_test.sh b/test/290_dns_fallback_test.sh index 4f57f811..0ea0cf03 100755 --- a/test/290_dns_fallback_test.sh +++ b/test/290_dns_fallback_test.sh @@ -4,7 +4,7 @@ start_suite "Resolve a non-weave address" -weave_on $HOST1 launch-dns 10.2.254.1/24 +weave_on $HOST1 launch-dns 10.2.254.1/24 --no-cache start_container_with_dns $HOST1 10.2.1.5/24 --name=c1 diff --git a/test/config.sh b/test/config.sh index 8e74c5ab..1ea8018e 100644 --- a/test/config.sh +++ b/test/config.sh @@ -115,10 +115,32 @@ container_ip() { weave_on $1 ps $2 | grep -o -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' } -# assert_dns_record +# assert_dns_record [ ...] assert_dns_record() { - assert "exec_on $1 $2 getent hosts $3 | tr -s ' '" "$4 $3" - assert "exec_on $1 $2 getent hosts $4 | tr -s ' '" "$4 $3" + host=$1 + container=$2 + name=$3 + shift 3 + exp_ips_regex=$(echo "$@" | sed -r 's/ /\\\|/g') + + [[ -z "$DEBUG" ]] || greyly echo "Checking whether $name exists at $host:$container" + got_ip=$(exec_on $host $container getent hosts $name) + assert_raises "echo \"$got_ip\" | grep -q \"$exp_ips_regex\"" + + [[ -z "$DEBUG" ]] || greyly echo "Checking whether the IPs \"$@\" exists at $host:$container" + for ip in "$@" ; do + assert "exec_on $host $container getent hosts $ip | tr -s ' '" "$ip $name" + done +} + +# assert_no_dns_record +assert_no_dns_record() { + host=$1 + container=$2 + name=$3 + + [[ -z "$DEBUG" ]] || greyly echo "Checking if \"$name\" does not exist at $host:$container" + assert_raises "exec_on $host $container getent hosts $name" 2 } start_suite() {