kvm/pkg/nmlite/resolvconf.go

210 lines
5.2 KiB
Go

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
}