Resolve PTR names over DNS-over-HTTPS and cache them for the record TTL (closes #238)
check / check (push) Successful in 4m39s
check / check (push) Successful in 4m39s
`ptr` keeps its field and shape. It now comes from Google, Cloudflare, AdGuard and Quad9 over RFC 8484, tried in turn within 2 seconds, in place of the host's resolver with no cache. Answers stay in memory and in a new `cache.db` for the record's TTL, at most 250,000 addresses. A reserved address is not sent to any resolver and has an empty `ptr`. Judgement call: each resolver gets an equal share of the time left, so a hung one does not use up the limit. Judgement call: the negative TTL is the smaller of the SOA minimum and the SOA's own TTL. Deviation: the cache is SQLite, by sneak's words, not the JSON file the standing rule asks for. Model: opus-5-5
This commit was merged in pull request #244.
This commit is contained in:
1 parent
641798bb22
commit
0cbd9cd943
21 files changed
+1973
-69
No files matched your search
@@ -25,6 +25,7 @@ import (
|
||||
"git.eeqj.de/sneak/routewatch/internal/iplist"
|
||||
"git.eeqj.de/sneak/routewatch/internal/logger"
|
||||
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
||||
"git.eeqj.de/sneak/routewatch/internal/ptr"
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
"git.eeqj.de/sneak/routewatch/internal/routetable"
|
||||
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
||||
@@ -557,8 +558,11 @@ func newServerWithTable(t *testing.T, table *routetable.Table) *Server {
|
||||
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
|
||||
return New(db, table, streamer.New(logger.New(), metrics.New()), logger.New(),
|
||||
s := New(db, table, streamer.New(logger.New(), metrics.New()), logger.New(),
|
||||
&config.Config{})
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// TestPrefixDetailHandlersParsePrefix checks that both prefix pages find a
|
||||
@@ -1252,7 +1256,7 @@ func TestIPLookupAnswers500WhenDatabaseReadFails(t *testing.T) {
|
||||
|
||||
s := New(failingIPLookupDB{}, newTestTable(),
|
||||
streamer.New(logger.New(), metrics.New()), logger.New(), &config.Config{})
|
||||
s.resolver = &fakeResolver{}
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
@@ -1288,16 +1292,19 @@ func TestIPLookupAnswers500WhenDatabaseReadFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// errNoSuchHost is what fakeResolver answers a lookup it has no answer for
|
||||
// with.
|
||||
// errNoSuchHost is what fakeResolver answers a host name lookup it has no
|
||||
// addresses for with.
|
||||
var errNoSuchHost = errors.New("no such host")
|
||||
|
||||
// fakeResolver answers a host name lookup with the addresses hosts lists for
|
||||
// the name, and a PTR lookup with no names or, when ptrNeverAnswers is set,
|
||||
// not at all: it waits until the lookup's context ends. hostLookups counts the
|
||||
// host name lookups.
|
||||
// the name. It answers a PTR lookup with the names ptrs lists for the address,
|
||||
// and with no names and no error, as the real one does, when ptrs lists none.
|
||||
// When ptrNeverAnswers is set, a PTR lookup fails after ptr.LookupTimeout, as
|
||||
// the real one does when no resolver answers. hostLookups counts the host name
|
||||
// lookups.
|
||||
type fakeResolver struct {
|
||||
hosts map[string][]string
|
||||
ptrs map[string][]string
|
||||
ptrNeverAnswers bool
|
||||
|
||||
hostLookups atomic.Int64
|
||||
@@ -1314,14 +1321,23 @@ func (r *fakeResolver) LookupHost(_ context.Context, host string) ([]string, err
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func (r *fakeResolver) LookupAddr(ctx context.Context, _ string) ([]string, error) {
|
||||
func (r *fakeResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) {
|
||||
if r.ptrNeverAnswers {
|
||||
ctx, cancel := context.WithTimeout(ctx, ptr.LookupTimeout)
|
||||
defer cancel()
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
return nil, errNoSuchHost
|
||||
return r.ptrs[addr], nil
|
||||
}
|
||||
|
||||
// useFakeResolver makes r answer the server's host name and PTR lookups.
|
||||
func useFakeResolver(s *Server, r *fakeResolver) {
|
||||
s.resolver = r
|
||||
s.ptrResolver = r
|
||||
}
|
||||
|
||||
// countingIPLookupDB embeds database.Store (left nil) and answers the IP
|
||||
@@ -1392,7 +1408,7 @@ func TestIPLookupRefusesTooManyAddresses(t *testing.T) {
|
||||
s := New(db, newTestTable(), streamer.New(logger.New(), metrics.New()),
|
||||
logger.New(), &config.Config{})
|
||||
resolver := &fakeResolver{hosts: hosts}
|
||||
s.resolver = resolver
|
||||
useFakeResolver(s, resolver)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
@@ -1427,18 +1443,19 @@ const addrInTestPrefixV4 = "192.0.2.1"
|
||||
|
||||
// TestIPLookupKeepsRouteWhenPTRLookupDoesNotAnswer looks up a routed address
|
||||
// with a resolver that never answers its PTR lookup. Once the PTR lookup's own
|
||||
// limit passes, the lookup answers 200 with the route. Before the fix the PTR
|
||||
// lookup ran until the request's time limit, and the lookup answered 504
|
||||
// without the route (https://git.eeqj.de/sneak/routewatch/issues/110).
|
||||
// limit passes, the lookup answers 200 with the route and an empty ptr. Before
|
||||
// the fix the PTR lookup ran until the request's time limit, and the lookup
|
||||
// answered 504 without the route
|
||||
// (https://git.eeqj.de/sneak/routewatch/issues/110).
|
||||
func TestIPLookupKeepsRouteWhenPTRLookupDoesNotAnswer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{ptrNeverAnswers: true}
|
||||
useFakeResolver(s, &fakeResolver{ptrNeverAnswers: true})
|
||||
|
||||
// The request's time limit passes well after the PTR lookup's, so that a
|
||||
// stall on a busy host does not use up the difference.
|
||||
const requestTimeLimit = ptrLookupTimeout + 15*time.Second
|
||||
const requestTimeLimit = ptr.LookupTimeout + 15*time.Second
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), requestTimeLimit)
|
||||
defer cancel()
|
||||
@@ -1464,6 +1481,10 @@ func TestIPLookupKeepsRouteWhenPTRLookupDoesNotAnswer(t *testing.T) {
|
||||
*body.Data.Results[0].Netblock != testPrefixV4 {
|
||||
t.Errorf("answer %s, want one result in %s", rec.Body, testPrefixV4)
|
||||
}
|
||||
|
||||
if !strings.Contains(rec.Body.String(), `"ptr":[]`) {
|
||||
t.Errorf("answer %s, want an empty ptr list", rec.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// addrInTestPrefixV6 is an address in testPrefixV6, which
|
||||
@@ -1541,7 +1562,7 @@ func TestIPLookupFlags(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{}
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
s.SetIPLists(fakeIPLists{})
|
||||
|
||||
tests := []struct {
|
||||
@@ -1654,7 +1675,7 @@ func TestIPLookupReportsIPLists(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{}
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
s.SetIPLists(fakeIPLists{})
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -1731,7 +1752,7 @@ func TestIPLookupReportsIPListsOfAnAddressWithNoRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{}
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
s.SetIPLists(fakeIPLists{})
|
||||
|
||||
// routeFields are the fields of a result that come from the address's
|
||||
@@ -1809,9 +1830,9 @@ func TestIPLookupResultsHaveTheSameFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{hosts: map[string][]string{
|
||||
useFakeResolver(s, &fakeResolver{hosts: map[string][]string{
|
||||
twoAddressHostName: {addrInTestPrefixV4, listedUnroutedAddr},
|
||||
}}
|
||||
}})
|
||||
s.SetIPLists(fakeIPLists{})
|
||||
|
||||
// want is the fields of the first result.
|
||||
@@ -1844,7 +1865,7 @@ func TestIPLookupOfAnAddressWithoutPTRNamesOrStoredNetblock(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{}
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
|
||||
path := "/api/v1/ip/" + addrInTestPrefixV4
|
||||
|
||||
@@ -1866,6 +1887,32 @@ func TestIPLookupOfAnAddressWithoutPTRNamesOrStoredNetblock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestIPLookupGivesPTRNamesWithoutTrailingDots looks up an address whose PTR
|
||||
// name the resolver gives fully qualified. The result's ptr holds the name
|
||||
// without its trailing dot.
|
||||
func TestIPLookupGivesPTRNamesWithoutTrailingDots(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
useFakeResolver(s, &fakeResolver{ptrs: map[string][]string{
|
||||
addrInTestPrefixV4: {"host.example.com."},
|
||||
}})
|
||||
|
||||
path := "/api/v1/ip/" + addrInTestPrefixV4
|
||||
|
||||
results := resultFields(t, get(t, s, path).Body.Bytes())
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("%s: %d results, want 1", path, len(results))
|
||||
}
|
||||
|
||||
got := string(results[0]["ptr"])
|
||||
|
||||
want := `["host.example.com"]`
|
||||
if got != want {
|
||||
t.Errorf("%s: ptr is %s, want %s", path, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIPLookupAnswersMayBeCached checks the Cache-Control header of the IP
|
||||
// lookup answers. Caches may keep them for the IP lists' shortest interval,
|
||||
// and only the caller's own browser may keep the answer about the caller's
|
||||
@@ -1874,7 +1921,7 @@ func TestIPLookupAnswersMayBeCached(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{}
|
||||
useFakeResolver(s, &fakeResolver{})
|
||||
s.SetIPLists(fakeIPLists{})
|
||||
|
||||
tests := []struct {
|
||||
@@ -1925,10 +1972,10 @@ func TestIPLookupHidesPrivateAddresses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{hosts: map[string][]string{
|
||||
useFakeResolver(s, &fakeResolver{hosts: map[string][]string{
|
||||
internalHostName: {internalHostAddr},
|
||||
"mixed.test": {internalHostAddr, addrInTestPrefixV4},
|
||||
}}
|
||||
}})
|
||||
|
||||
notPublic := internalHostName + ": " + hostNotPublicMessage
|
||||
|
||||
|
||||
Reference in new issue
Block a user