Reverse lookups (internal/names, POST /names) put a name beside each inside address in connections, the log, quota usage, the dashboard's quotas and the statistics' devices and blocked sources, in the web UI and hc (--no-names to leave them out). Only inside addresses are looked up: an inside zone's networks, and private ones outside the external zones'. Names are cached (names.cache, 10m) and never stored; names.resolver picks the DNS server. Also: hc stats top devices listed each device twice; the dashboard's quotas named the rule's ID twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
// Package names finds the names of addresses by reverse lookups, for
|
|
// showing them beside the addresses (connections, the log, quotas,
|
|
// traffic per device). Names are cached and never stored: statistics stay
|
|
// keyed by address.
|
|
package names
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Lookups running at once, and the most names kept.
|
|
const (
|
|
parallel = 16
|
|
maxCache = 20000
|
|
)
|
|
|
|
// Resolver looks up and caches addresses' names.
|
|
type Resolver struct {
|
|
lookup func(ctx context.Context, addr string) ([]string, error)
|
|
// ttl is how long a name (or its absence) is kept; a lookup that
|
|
// failed (a timeout, a server error) is tried again after retry.
|
|
ttl, retry time.Duration
|
|
timeout time.Duration
|
|
now func() time.Time
|
|
|
|
mu sync.Mutex
|
|
cache map[netip.Addr]entry
|
|
pending map[netip.Addr]chan struct{}
|
|
sem chan struct{}
|
|
}
|
|
|
|
type entry struct {
|
|
name string
|
|
expires time.Time
|
|
}
|
|
|
|
// New makes a resolver asking server ("10.10.0.1:53"; "" for the
|
|
// system's, /etc/hosts then /etc/resolv.conf), keeping names for ttl.
|
|
func New(server string, ttl time.Duration) *Resolver {
|
|
res := &net.Resolver{PreferGo: true}
|
|
if server != "" {
|
|
res.Dial = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
|
var d net.Dialer
|
|
return d.DialContext(ctx, network, server)
|
|
}
|
|
}
|
|
return NewFunc(res.LookupAddr, ttl)
|
|
}
|
|
|
|
// NewFunc makes a resolver with its own lookup (tests).
|
|
func NewFunc(lookup func(ctx context.Context, addr string) ([]string, error), ttl time.Duration) *Resolver {
|
|
return &Resolver{lookup: lookup, ttl: ttl, retry: time.Minute, timeout: 2 * time.Second, now: time.Now,
|
|
cache: map[netip.Addr]entry{}, pending: map[netip.Addr]chan struct{}{}, sem: make(chan struct{}, parallel)}
|
|
}
|
|
|
|
// Names returns the names of addrs: those cached, and those looked up
|
|
// within wait. Lookups still running then carry on and are cached for
|
|
// the next call. An address without a name is left out.
|
|
func (r *Resolver) Names(ctx context.Context, addrs []netip.Addr, wait time.Duration) map[netip.Addr]string {
|
|
out := map[netip.Addr]string{}
|
|
var waiting []netip.Addr
|
|
var done []chan struct{}
|
|
now := r.now()
|
|
r.mu.Lock()
|
|
for _, a := range addrs {
|
|
if e, ok := r.cache[a]; ok && now.Before(e.expires) {
|
|
if e.name != "" {
|
|
out[a] = e.name
|
|
}
|
|
continue
|
|
}
|
|
ch, ok := r.pending[a]
|
|
if !ok {
|
|
ch = make(chan struct{})
|
|
r.pending[a] = ch
|
|
go r.resolve(a, ch)
|
|
}
|
|
waiting = append(waiting, a)
|
|
done = append(done, ch)
|
|
}
|
|
r.mu.Unlock()
|
|
if len(waiting) == 0 {
|
|
return out
|
|
}
|
|
t := time.NewTimer(wait)
|
|
defer t.Stop()
|
|
for _, ch := range done {
|
|
select {
|
|
case <-ch:
|
|
case <-t.C:
|
|
return r.collect(out, waiting)
|
|
case <-ctx.Done():
|
|
return r.collect(out, waiting)
|
|
}
|
|
}
|
|
return r.collect(out, waiting)
|
|
}
|
|
|
|
// collect adds what the lookups found for addrs.
|
|
func (r *Resolver) collect(out map[netip.Addr]string, addrs []netip.Addr) map[netip.Addr]string {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for _, a := range addrs {
|
|
if e, ok := r.cache[a]; ok && e.name != "" {
|
|
out[a] = e.name
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Resolver) resolve(a netip.Addr, done chan struct{}) {
|
|
r.sem <- struct{}{}
|
|
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
|
|
ns, err := r.lookup(ctx, a.String())
|
|
cancel()
|
|
<-r.sem
|
|
e := entry{expires: r.now().Add(r.ttl)}
|
|
var dnsErr *net.DNSError
|
|
switch {
|
|
case err == nil && len(ns) > 0:
|
|
e.name = strings.TrimSuffix(ns[0], ".")
|
|
case err != nil && !(errors.As(err, &dnsErr) && dnsErr.IsNotFound):
|
|
e.expires = r.now().Add(r.retry)
|
|
}
|
|
r.mu.Lock()
|
|
if len(r.cache) >= maxCache {
|
|
r.prune()
|
|
}
|
|
r.cache[a] = e
|
|
delete(r.pending, a)
|
|
r.mu.Unlock()
|
|
close(done)
|
|
}
|
|
|
|
// prune drops expired names, or all of them when none has expired.
|
|
func (r *Resolver) prune() {
|
|
now := r.now()
|
|
for a, e := range r.cache {
|
|
if !now.Before(e.expires) {
|
|
delete(r.cache, a)
|
|
}
|
|
}
|
|
if len(r.cache) >= maxCache {
|
|
clear(r.cache)
|
|
}
|
|
}
|