[vnet] Skip rewriting /etc/resolver files when DNS config is unchanged on macOS (#68746) (#632)

Export-Source-Commit: 01e89c89ecc67620917c3ac4e38455ca405fa884
This commit is contained in:
Pavel
2026-08-25 12:19:47 -05:00
committed by Gus Rivera
parent 276e9adbc5
commit d81947867e
+95 -26
View File
@@ -36,6 +36,15 @@ type platformOSConfigState struct {
configuredIPv6 bool
configuredIPv6Route bool
configuredCidrRanges []string
configuredDNS *appliedDNSConfig
}
// appliedDNSConfig records what configureDNS last wrote.
type appliedDNSConfig struct {
// fileContents is the content written to every managed resolver file.
fileContents []byte
// files holds the sorted absolute paths of the managed resolver files.
files []string
}
// platformConfigureOS reconciles host OS state for cfg. It is safe
@@ -45,7 +54,7 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo
// IPs and routes are cleaned up when the TUN is deleted on exit,
// so no removal is needed.
if cfg.tunName == "" {
if err := configureDNS(ctx, cfg.dnsAddrs, cfg.dnsZones); err != nil {
if err := configureDNS(ctx, state, cfg.dnsAddrs, cfg.dnsZones); err != nil {
return trace.Wrap(err, "configuring DNS")
}
*state = platformOSConfigState{}
@@ -100,7 +109,7 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo
state.configuredIPv6Route = true
}
if err := configureDNS(ctx, cfg.dnsAddrs, cfg.dnsZones); err != nil {
if err := configureDNS(ctx, state, cfg.dnsAddrs, cfg.dnsZones); err != nil {
return trace.Wrap(err, "configuring DNS")
}
@@ -117,19 +126,38 @@ const resolverFileComment = "# automatically installed by Teleport VNet"
var resolverPath = filepath.Join("/", "etc", "resolver")
func configureDNS(ctx context.Context, nameservers []string, zones []string) error {
// configureDNS writes resolver files for the zones to /etc/resolver, skipping
// the write when nothing changed. A change in nameserver order triggers a rewrite.
func configureDNS(ctx context.Context, state *platformOSConfigState, nameservers []string, zones []string) error {
if len(nameservers) == 0 {
// There are no nameservers so VNet can't handle any DNS zones. Continue
// so that any VNet-managed resolver files can be deleted.
zones = nil
}
desiredContents := desiredResolverFileContents(nameservers)
files := make([]string, 0, len(zones))
for _, zone := range zones {
files = append(files, filepath.Join(resolverPath, zone))
}
slices.Sort(files)
if state.configuredDNS != nil &&
bytes.Equal(state.configuredDNS.fileContents, desiredContents) &&
slices.Equal(state.configuredDNS.files, files) {
// Read the managed files to catch external drift
if resolverFilesMatch(ctx, files, desiredContents) {
return nil
}
log.InfoContext(ctx, "Resolver files no longer match the applied DNS configuration, re-applying.")
}
log.DebugContext(ctx, "Configuring DNS.", "nameservers", nameservers, "zones", zones)
if err := os.MkdirAll(resolverPath, os.FileMode(0755)); err != nil {
return trace.Wrap(err, "creating %s", resolverPath)
}
managedFiles, err := vnetManagedResolverFiles()
managedFiles, err := vnetManagedResolverFiles(ctx)
if err != nil {
return trace.Wrap(err, "finding VNet managed files in /etc/resolver")
}
@@ -138,18 +166,8 @@ func configureDNS(ctx context.Context, nameservers []string, zones []string) err
// errors with one or more of them.
var allErrors []error
var fileContents bytes.Buffer
fileContents.WriteString(resolverFileComment)
fileContents.WriteByte('\n')
for _, nameserver := range nameservers {
fileContents.WriteString("nameserver ")
fileContents.WriteString(nameserver)
fileContents.WriteByte('\n')
}
for _, zone := range zones {
fileName := filepath.Join(resolverPath, zone)
if err := renameio.WriteFile(fileName, fileContents.Bytes(), 0644); err != nil {
for _, fileName := range files {
if err := renameio.WriteFile(fileName, desiredContents, 0644); err != nil {
allErrors = append(allErrors, trace.Wrap(err, "writing DNS configuration file %s", fileName))
} else {
// Successfully wrote this file, don't clean it up below.
@@ -164,10 +182,45 @@ func configureDNS(ctx context.Context, nameservers []string, zones []string) err
}
}
if len(allErrors) == 0 {
state.configuredDNS = &appliedDNSConfig{
fileContents: desiredContents,
files: files,
}
}
return trace.NewAggregate(allErrors...)
}
func vnetManagedResolverFiles() (map[string]struct{}, error) {
func desiredResolverFileContents(nameservers []string) []byte {
var fileContents bytes.Buffer
fileContents.WriteString(resolverFileComment)
fileContents.WriteByte('\n')
for _, nameserver := range nameservers {
fileContents.WriteString("nameserver ")
fileContents.WriteString(nameserver)
fileContents.WriteByte('\n')
}
return fileContents.Bytes()
}
// resolverFilesMatch reports whether every managed resolver file still has
// the expected contents.
func resolverFilesMatch(ctx context.Context, files []string, wantContents []byte) bool {
for _, f := range files {
contents, err := os.ReadFile(f)
if err != nil {
log.DebugContext(ctx, "Failed to read resolver file.", "file", f, "error", err)
return false
}
if !bytes.Equal(contents, wantContents) {
log.DebugContext(ctx, "Resolver file does not have the expected contents.", "file", f)
return false
}
}
return true
}
func vnetManagedResolverFiles(ctx context.Context) (map[string]struct{}, error) {
entries, err := os.ReadDir(resolverPath)
if err != nil {
return nil, trace.Wrap(err, "reading %s", resolverPath)
@@ -179,18 +232,34 @@ func vnetManagedResolverFiles() (map[string]struct{}, error) {
continue
}
filePath := filepath.Join(resolverPath, entry.Name())
file, err := os.Open(filePath)
matches, err := fileStartsWithVNetComment(ctx, filePath)
if err != nil {
return nil, trace.Wrap(err, "opening %s", filePath)
return nil, trace.Wrap(err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
if scanner.Scan() {
if resolverFileComment == scanner.Text() {
matchingFiles[filePath] = struct{}{}
}
if matches {
matchingFiles[filePath] = struct{}{}
}
}
return matchingFiles, nil
}
// fileStartsWithVNetComment reports whether the first line of the file at
// path is the comment marking a VNet managed resolver file. Files whose
// first line cannot be read are treated as not managed by VNet.
func fileStartsWithVNetComment(ctx context.Context, path string) (bool, error) {
file, err := os.Open(path)
if err != nil {
return false, trace.Wrap(err, "opening %s", path)
}
defer file.Close()
scanner := bufio.NewScanner(file)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
log.DebugContext(ctx, "Failed to read first line of resolver file, treating it as not managed by VNet.",
"file", path, "error", err)
}
return false, nil
}
return scanner.Text() == resolverFileComment, nil
}