Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,20 @@ name that only exists while the host is up cannot be used to wake it. Give the
machine a DHCP reservation and a static DNS entry, or write the IP straight into
the config.

### Names on the near side

By default doormouse logs only the client's IP. Set `client_hostname_lookup =
true` to also log the client's hostname. doormouse gets it with a reverse-DNS
(PTR) lookup, so the request log shows which machine or service sent the
traffic.

Lookups are best-effort and cached: hits for 30 minutes, failures for 2.
Concurrent requests from the same uncached IP share one lookup.

> [!WARNING]
> The lookup is synchronous. On a cache miss, doormouse waits up to one second
> for it before it logs the request and forwards it. That is why the setting is
> opt-in. Leave it off if your network has no PTR records or slow DNS.
## Liveness and readiness

There are two `health_check` fields and they answer different questions.
Expand Down
91 changes: 91 additions & 0 deletions client_resolver_regression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"fmt"
"sync"
"testing"
"time"
)

func TestClientResolverBoundsConcurrentDistinctLookups(t *testing.T) {
resolver := newClientResolver(true)
started := make(chan struct{}, 65)
release := make(chan struct{})
var workers sync.WaitGroup
t.Cleanup(func() {
close(release)
workers.Wait()
})
resolver.lookupFn = func(string, time.Duration) (string, error) {
started <- struct{}{}
<-release
return "nas.lan", nil
}
for index := 0; index < 64; index++ {
workers.Add(1)
go func() {
defer workers.Done()
resolver.resolve(fmt.Sprintf("192.0.2.%d", index+1))
}()
select {
case <-started:
case <-time.After(time.Second):
t.Fatal("lookup did not start below the limit")
}
}

done := make(chan bool, 1)
workers.Add(1)
go func() {
defer workers.Done()
_, ok := resolver.resolve("192.0.2.100")
done <- ok
}()
select {
case <-started:
t.Fatal("started DNS work beyond the 64-lookup limit")
case ok := <-done:
if ok {
t.Fatal("overloaded lookup should fall back to the IP")
}
case <-time.After(time.Second):
t.Fatal("overloaded lookup should not wait for capacity")
}
if resolver.hasKey("192.0.2.100") {
t.Fatal("overload must not be cached as a DNS failure")
}
}

func TestClientResolverEvictsExpiredFailureBehindFreshSuccess(t *testing.T) {
resolver := newClientResolver(true)
resolver.maxEntries = 2
resolver.store("192.0.2.1", "nas.lan")
resolver.store("192.0.2.2", "")
resolver.ageKey("192.0.2.1", 3*time.Minute)
resolver.ageKey("192.0.2.2", 3*time.Minute)
resolver.store("192.0.2.3", "new.lan")

if host, ok := resolver.cached("192.0.2.1"); !ok || host != "nas.lan" {
t.Fatal("fresh success evicted while an expired failure existed")
}
if resolver.hasKey("192.0.2.2") || resolver.cacheSize() != 2 {
t.Fatal("expired failure must be removed and cache stay at capacity")
}
}

func TestClientResolverRechecksCacheBeforeStartingLookup(t *testing.T) {
resolver := newClientResolver(true)
for _, hostname := range []string{"nas.lan", ""} {
resolver.store("192.0.2.1", hostname)
calls := 0
resolver.lookupFn = func(string, time.Duration) (string, error) {
calls++
return "unexpected.lan", nil
}

got, err := resolver.lookupOnce("192.0.2.1")
if err != nil || got != hostname || calls != 0 {
t.Fatalf("lookup after another caller populated cache = (%q, %v), calls = %d; want (%q, nil), no calls", got, err, calls, hostname)
}
}
}
Loading
Loading