net/dns: cache dns.Config for reuse when compileConfig fails (#16059)

fixes tailscale/corp#25612

We now keep track of any dns configurations which we could not
compile. This gives RecompileDNSConfig a configuration to
attempt to recompile and apply when the OS pokes us to indicate
that the interface dns servers have changed/updated.   The manager config
will remain unset until we have the required information to compile
it correctly which should eliminate the problematic SERVFAIL
responses (especially on macOS 15).

This also removes the missingUpstreamRecovery func in the forwarder
which is no longer required now that we have proper error handling
and recovery manager and the client.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
Jonathan Nobels
2025-05-28 15:43:12 -04:00
committed by GitHub
parent ffc8ec289b
commit 5e54819cee
4 changed files with 76 additions and 61 deletions
+54 -2
View File
@@ -4,6 +4,7 @@
package dns
import (
"errors"
"net/netip"
"runtime"
"strings"
@@ -24,8 +25,9 @@ type fakeOSConfigurator struct {
SplitDNS bool
BaseConfig OSConfig
OSConfig OSConfig
ResolverConfig resolver.Config
OSConfig OSConfig
ResolverConfig resolver.Config
GetBaseConfigErr *error
}
func (c *fakeOSConfigurator) SetDNS(cfg OSConfig) error {
@@ -45,6 +47,9 @@ func (c *fakeOSConfigurator) SupportsSplitDNS() bool {
}
func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
if c.GetBaseConfigErr != nil {
return OSConfig{}, *c.GetBaseConfigErr
}
return c.BaseConfig, nil
}
@@ -1019,3 +1024,50 @@ func upstreams(strs ...string) (ret map[dnsname.FQDN][]*dnstype.Resolver) {
}
return ret
}
func TestConfigRecompilation(t *testing.T) {
fakeErr := errors.New("fake os configurator error")
f := &fakeOSConfigurator{}
f.GetBaseConfigErr = &fakeErr
f.BaseConfig = OSConfig{
Nameservers: mustIPs("1.1.1.1"),
}
config := Config{
Routes: upstreams("ts.net", "69.4.2.0", "foo.ts.net", ""),
SearchDomains: fqdns("foo.ts.net"),
}
m := NewManager(t.Logf, f, new(health.Tracker), tsdial.NewDialer(netmon.NewStatic()), nil, nil, "darwin")
var managerConfig *resolver.Config
m.resolver.TestOnlySetHook(func(cfg resolver.Config) {
managerConfig = &cfg
})
// Initial set should error out and store the config
if err := m.Set(config); err == nil {
t.Fatalf("Want non-nil error. Got nil")
}
if m.config == nil {
t.Fatalf("Want persisted config. Got nil.")
}
if managerConfig != nil {
t.Fatalf("Want nil managerConfig. Got %v", managerConfig)
}
// Clear the error. We should take the happy path now and
// set m.manager's Config.
f.GetBaseConfigErr = nil
// Recompilation without an error should succeed and set m.config and m.manager's [resolver.Config]
if err := m.RecompileDNSConfig(); err != nil {
t.Fatalf("Want nil error. Got err %v", err)
}
if m.config == nil {
t.Fatalf("Want non-nil config. Got nil")
}
if managerConfig == nil {
t.Fatalf("Want non nil managerConfig. Got nil")
}
}