package nmlite import ( "bytes" "fmt" "html/template" "os" "strings" "github.com/jetkvm/kvm/internal/network/types" "github.com/jetkvm/kvm/internal/sync" "github.com/jetkvm/kvm/pkg/nmlite/link" "github.com/rs/zerolog" ) const ( resolvConfPath = "/etc/resolv.conf" resolvConfFileMode = 0644 resolvConfTemplate = `# the resolv.conf file is managed by JetKVM # DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN {{ if .searchList }} search {{ join .searchList " " }} {{- end -}} {{ if .domain }} domain {{ .domain }} {{- end -}} {{ range $ns, $comment := .nameservers }} nameserver {{ printf "%s" $ns }} # {{ join $comment ", " }} {{- end }} ` ) var ( tplFuncMap = template.FuncMap{ "join": strings.Join, } ) // ResolvConfManager manages the resolv.conf file type ResolvConfManager struct { logger *zerolog.Logger mu sync.Mutex conf *types.ResolvConf hostname string domain string } // NewResolvConfManager creates a new resolv.conf manager func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager { if logger == nil { // Create a no-op logger if none provided logger = &zerolog.Logger{} } return &ResolvConfManager{ logger: logger, mu: sync.Mutex{}, conf: &types.ResolvConf{ ConfigIPv4: make(map[string]types.InterfaceResolvConf), ConfigIPv6: make(map[string]types.InterfaceResolvConf), }, } } // SetInterfaceConfig sets the resolv.conf configuration for a specific interface func (rcm *ResolvConfManager) SetInterfaceConfig(iface string, family int, config types.InterfaceResolvConf) error { // DO NOT USE defer HERE, rcm.update() also locks the mutex rcm.mu.Lock() switch family { case link.AfInet: rcm.conf.ConfigIPv4[iface] = config case link.AfInet6: rcm.conf.ConfigIPv6[iface] = config default: rcm.mu.Unlock() return fmt.Errorf("invalid family: %d", family) } rcm.mu.Unlock() if err := rcm.reconcileHostname(); err != nil { return fmt.Errorf("failed to reconcile hostname: %w", err) } return rcm.update() } // SetConfig sets the resolv.conf configuration func (rcm *ResolvConfManager) SetConfig(resolvConf *types.ResolvConf) error { if resolvConf == nil { return fmt.Errorf("resolvConf cannot be nil") } rcm.mu.Lock() rcm.conf = resolvConf defer rcm.mu.Unlock() return rcm.update() } // Reconcile reconciles the resolv.conf configuration func (rcm *ResolvConfManager) Reconcile() error { if err := rcm.reconcileHostname(); err != nil { return fmt.Errorf("failed to reconcile hostname: %w", err) } return rcm.update() } // Update updates the resolv.conf file func (rcm *ResolvConfManager) update() error { rcm.mu.Lock() defer rcm.mu.Unlock() rcm.logger.Debug().Msg("updating resolv.conf") // Generate resolv.conf content content, err := rcm.generateResolvConf(rcm.conf) if err != nil { return fmt.Errorf("failed to generate resolv.conf: %w", err) } // Check if the file is the same if _, err := os.Stat(resolvConfPath); err == nil { existingContent, err := os.ReadFile(resolvConfPath) if err != nil { rcm.logger.Warn().Err(err).Msg("failed to read existing resolv.conf") } if bytes.Equal(existingContent, content) { rcm.logger.Debug().Msg("resolv.conf is the same, skipping write") return nil } } // Write to file if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil { return fmt.Errorf("failed to write resolv.conf: %w", err) } rcm.logger.Info(). Interface("config", rcm.conf). Msg("resolv.conf updated successfully") return nil } type configMap map[string][]string func mergeConfig(nameservers *configMap, searchList *configMap, config *types.InterfaceResolvConfMap) { localNameservers := *nameservers localSearchList := *searchList for ifname, iface := range *config { comment := ifname if iface.Source != "" { comment += fmt.Sprintf(" (%s)", iface.Source) } for _, ip := range iface.NameServers { ns := ip.String() if _, ok := localNameservers[ns]; !ok { localNameservers[ns] = []string{} } localNameservers[ns] = append(localNameservers[ns], comment) } for _, search := range iface.SearchList { search = strings.Trim(search, ".") if _, ok := localSearchList[search]; !ok { localSearchList[search] = []string{} } localSearchList[search] = append(localSearchList[search], comment) } } *nameservers = localNameservers *searchList = localSearchList } // generateResolvConf generates resolv.conf content func (rcm *ResolvConfManager) generateResolvConf(conf *types.ResolvConf) ([]byte, error) { tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate) if err != nil { return nil, fmt.Errorf("failed to parse template: %w", err) } // merge the nameservers and searchList nameservers := configMap{} searchList := configMap{} mergeConfig(&nameservers, &searchList, &conf.ConfigIPv4) mergeConfig(&nameservers, &searchList, &conf.ConfigIPv6) flattenedSearchList := []string{} for search := range searchList { flattenedSearchList = append(flattenedSearchList, search) } var buf bytes.Buffer if err := tmpl.Execute(&buf, map[string]any{ "nameservers": nameservers, "searchList": flattenedSearchList, }); err != nil { return nil, fmt.Errorf("failed to execute template: %w", err) } return buf.Bytes(), nil }