커밋 비교
1
커밋
| 작성자 | SHA1 | 날짜 | |
|---|---|---|---|
|
|
017eede779 |
@@ -110,9 +110,7 @@ Contributions that introduce mocked, faked, or stubbed DNS will be rejected.
|
|||||||
- Port transitioned from open to closed (or vice versa).
|
- Port transitioned from open to closed (or vice versa).
|
||||||
- New IP appeared (from DNS change) and its port state was recorded.
|
- New IP appeared (from DNS change) and its port state was recorded.
|
||||||
- IP disappeared (from DNS change) — noted in the DNS change notification;
|
- IP disappeared (from DNS change) — noted in the DNS change notification;
|
||||||
port state for that IP is removed. When none of a name's nameservers
|
port state for that IP is removed.
|
||||||
answered, its addresses are not known, so the port state saved for them is
|
|
||||||
kept.
|
|
||||||
|
|
||||||
### TLS Certificate Monitoring
|
### TLS Certificate Monitoring
|
||||||
|
|
||||||
@@ -402,9 +400,8 @@ performs full iterative resolution:
|
|||||||
directly for the requested records.
|
directly for the requested records.
|
||||||
|
|
||||||
In steps 2 and 3 the servers are asked one at a time in a random order, chosen
|
In steps 2 and 3 the servers are asked one at a time in a random order, chosen
|
||||||
anew each time, so no one root server gets every first query. A server that does
|
anew each time, until one replies; the lookup fails only when none does. No one
|
||||||
not reply, or refuses the query, is passed over for the next one; the first
|
root server gets every first query.
|
||||||
other reply is used, even a SERVFAIL, and no further server is asked.
|
|
||||||
|
|
||||||
This approach ensures:
|
This approach ensures:
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
- 2026-10-01: the resolver tries root servers, and every other server list it
|
- 2026-10-01: the resolver tries root servers, and every other server list it
|
||||||
walks, in a random order each time, not always from the top (closes #138).
|
walks, in a random order each time, not always from the top (closes #138).
|
||||||
- 2026-10-01: when none of a configured name's nameservers answered, the port
|
|
||||||
state saved for its addresses is kept, not removed (closes #193).
|
|
||||||
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
- 2026-10-01: `ResolveIPAddresses` returns an error, not no addresses, when no
|
||||||
nameserver of the name's zone answered (closes #190).
|
nameserver of the name's zone answered (closes #190).
|
||||||
- 2026-10-01: `make fmt` and `make fmt-check` cover Markdown with prettier, run
|
- 2026-10-01: `make fmt` and `make fmt-check` cover Markdown with prettier, run
|
||||||
|
|||||||
@@ -67,11 +67,6 @@ func (w *Watcher) DetectNSAddressChanges(
|
|||||||
w.detectNSAddressChanges(ctx, domain, prev, current)
|
w.detectNSAddressChanges(ctx, domain, prev, current)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAllPorts exports checkAllPorts for testing.
|
|
||||||
func (w *Watcher) CheckAllPorts(ctx context.Context) {
|
|
||||||
w.checkAllPorts(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildHostnameState exports buildHostnameState for testing.
|
// BuildHostnameState exports buildHostnameState for testing.
|
||||||
func BuildHostnameState(
|
func BuildHostnameState(
|
||||||
results map[string]*resolver.NameserverResponse,
|
results map[string]*resolver.NameserverResponse,
|
||||||
|
|||||||
@@ -331,118 +331,3 @@ func TestNameserverThatRefuses(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPortStateWhenNoNameserverAnswered runs the port checks on
|
|
||||||
// hostname state built here, which gives the name no address. The port
|
|
||||||
// state saved for its old address is kept only when the name is a
|
|
||||||
// configured hostname or domain and none of its nameservers answered.
|
|
||||||
func TestPortStateWhenNoNameserverAnswered(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
noneAnswered := saved(map[string]*state.NameserverRecordState{
|
|
||||||
nsA: failed(), nsB: failed(),
|
|
||||||
})
|
|
||||||
oneAnsweredNoAddress := saved(map[string]*state.NameserverRecordState{
|
|
||||||
nsA: answered(map[string][]string{}), nsB: failed(),
|
|
||||||
})
|
|
||||||
|
|
||||||
configured := []string{host}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
hostname *state.HostnameState
|
|
||||||
hostnames []string
|
|
||||||
domains []string
|
|
||||||
wantKept bool
|
|
||||||
}{
|
|
||||||
{"no nameserver answered", noneAnswered, configured, nil, true},
|
|
||||||
{
|
|
||||||
"no nameserver answered, configured as a domain",
|
|
||||||
noneAnswered, nil, configured, true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"one answered with no address",
|
|
||||||
oneAnsweredNoAddress, configured, nil, false,
|
|
||||||
},
|
|
||||||
{"no nameserver answered, not configured", noneAnswered, nil, nil, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cfg := defaultTestConfig(t)
|
|
||||||
cfg.Hostnames = tt.hostnames
|
|
||||||
cfg.Domains = tt.domains
|
|
||||||
|
|
||||||
// The port checks read the saved hostname state and look
|
|
||||||
// nothing up, so the watcher has no resolver.
|
|
||||||
deps := newTestDeps(t, cfg)
|
|
||||||
w := watcher.NewForTest(
|
|
||||||
cfg, deps.state, nil,
|
|
||||||
deps.portChecker, deps.tlsChecker, deps.notifier,
|
|
||||||
)
|
|
||||||
|
|
||||||
key := ip1 + ":443"
|
|
||||||
|
|
||||||
deps.state.SetHostnameState(host, tt.hostname)
|
|
||||||
deps.state.SetPortState(key, &state.PortState{
|
|
||||||
Open: true, Hostnames: []string{host},
|
|
||||||
})
|
|
||||||
|
|
||||||
w.CheckAllPorts(t.Context())
|
|
||||||
|
|
||||||
_, kept := deps.state.GetPortState(key)
|
|
||||||
if kept != tt.wantKept {
|
|
||||||
t.Errorf("port state %s kept: %v, want %v", key, kept, tt.wantKept)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPortStateWhenNoNameserverAnsweredAndOtherNameMovesAway saves the
|
|
||||||
// port state of an address two configured hostnames resolve to. While
|
|
||||||
// none of the first one's nameservers answer, the port checks run with
|
|
||||||
// the other one still at that address, then after it moved away; the
|
|
||||||
// port state is kept both times.
|
|
||||||
func TestPortStateWhenNoNameserverAnsweredAndOtherNameMovesAway(
|
|
||||||
t *testing.T,
|
|
||||||
) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const other = "mail.example.net"
|
|
||||||
|
|
||||||
cfg := defaultTestConfig(t)
|
|
||||||
cfg.Hostnames = []string{host, other}
|
|
||||||
|
|
||||||
// The port checks read the saved hostname state and look nothing
|
|
||||||
// up, so the watcher has no resolver.
|
|
||||||
deps := newTestDeps(t, cfg)
|
|
||||||
w := watcher.NewForTest(
|
|
||||||
cfg, deps.state, nil,
|
|
||||||
deps.portChecker, deps.tlsChecker, deps.notifier,
|
|
||||||
)
|
|
||||||
|
|
||||||
key := ip1 + ":443"
|
|
||||||
|
|
||||||
deps.state.SetPortState(key, &state.PortState{
|
|
||||||
Open: true, Hostnames: []string{host, other},
|
|
||||||
})
|
|
||||||
deps.state.SetHostnameState(host, saved(
|
|
||||||
map[string]*state.NameserverRecordState{nsA: failed(), nsB: failed()},
|
|
||||||
))
|
|
||||||
|
|
||||||
for _, otherIP := range []string{ip1, ip2} {
|
|
||||||
deps.state.SetHostnameState(other, saved(
|
|
||||||
map[string]*state.NameserverRecordState{
|
|
||||||
nsA: answered(map[string][]string{"A": {otherIP}}),
|
|
||||||
},
|
|
||||||
))
|
|
||||||
|
|
||||||
w.CheckAllPorts(t.Context())
|
|
||||||
|
|
||||||
if _, kept := deps.state.GetPortState(key); !kept {
|
|
||||||
t.Fatalf("port state %s removed with %s at %s", key, other, otherIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+4
-51
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -709,46 +708,15 @@ func parsePortKey(key string) (string, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cleanupStalePorts removes port state entries that are no
|
// cleanupStalePorts removes port state entries that are no
|
||||||
// longer referenced by any hostname in the current DNS data. An
|
// longer referenced by any hostname in the current DNS data.
|
||||||
// entry saved for a configured name none of whose nameservers
|
|
||||||
// answered is kept: that name's addresses are not known, not gone.
|
|
||||||
func (w *Watcher) cleanupStalePorts(
|
func (w *Watcher) cleanupStalePorts(
|
||||||
currentAssociations map[string][]string,
|
currentAssociations map[string][]string,
|
||||||
) {
|
) {
|
||||||
for _, key := range w.state.GetAllPortKeys() {
|
for _, key := range w.state.GetAllPortKeys() {
|
||||||
if _, exists := currentAssociations[key]; exists {
|
if _, exists := currentAssociations[key]; !exists {
|
||||||
continue
|
w.state.DeletePortState(key)
|
||||||
}
|
|
||||||
|
|
||||||
ps, ok := w.state.GetPortState(key)
|
|
||||||
if ok && slices.ContainsFunc(ps.Hostnames, w.noNameserverAnswered) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w.state.DeletePortState(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// noNameserverAnswered reports whether name is a configured domain or
|
|
||||||
// hostname and none of its nameservers answered on its last check.
|
|
||||||
func (w *Watcher) noNameserverAnswered(name string) bool {
|
|
||||||
if !slices.Contains(w.config.Hostnames, name) &&
|
|
||||||
!slices.Contains(w.config.Domains, name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hs, ok := w.state.GetHostnameState(name)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nsState := range hs.RecordsByNameserver {
|
|
||||||
if nsState.Status == statusOK {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Watcher) collectIPs(hostname string) []string {
|
func (w *Watcher) collectIPs(hostname string) []string {
|
||||||
@@ -827,24 +795,9 @@ func (w *Watcher) checkSinglePort(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A configured name on the saved list none of whose nameservers
|
|
||||||
// answered stays on it, so the entry is kept when the other names
|
|
||||||
// stop resolving to this address.
|
|
||||||
savedHostnames := slices.Clone(hostnames)
|
|
||||||
|
|
||||||
if hasPrev {
|
|
||||||
for _, name := range prev.Hostnames {
|
|
||||||
if !slices.Contains(hostnames, name) && w.noNameserverAnswered(name) {
|
|
||||||
savedHostnames = append(savedHostnames, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(savedHostnames)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.state.SetPortState(key, &state.PortState{
|
w.state.SetPortState(key, &state.PortState{
|
||||||
Open: result.Open,
|
Open: result.Open,
|
||||||
Hostnames: savedHostnames,
|
Hostnames: hostnames,
|
||||||
LastChecked: now,
|
LastChecked: now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
새 이슈에서 참조
사용자 차단