go-network-configurator/networkScripts.go
James Coleman fae43247de Switch unit tests to testify
Bring an administratively down interface up in AddAddress before writing
its addresses and routes. The kernel installs an address's connected
route only while its link is up, so adding a gateway to a down interface
was rejected as unreachable. A link raised this way is part of the
pre-change state AddAddress restores, so it is returned to down on any
path that does not complete.

Also fix the rollback masking its own error: deleting the address it
added takes the default route with it, since that route resolved its
nexthop through the address's connected subnet. The following RouteDel
then answered ESRCH and was reported in place of "aborted operation due
to loss of internet".
2026-07-10 08:52:52 -05:00

998 lines
25 KiB
Go

package netconfig
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
"time"
"unicode"
"dario.cat/mergo"
"github.com/vishvananda/netlink"
)
const networkScriptsPath = "/etc/sysconfig/network-scripts"
type nsFile struct {
Name string
Config map[string]string
}
type nsRouteFile struct {
Name string
Routes [][]string
}
type nsInterface struct {
Name string
IFFiles []nsFile
Route4File nsRouteFile
Route6File nsRouteFile
}
type networkScripts struct {
Interfaces []*nsInterface
ConfigDir string
backupRetention int
}
// nsDHCPState reports whether an ifcfg file's keys enable a DHCP client for
// each family. BOOTPROTO names the IPv4 method — "dhcp" and the near-extinct
// "bootp" are the dynamic ones, and any other value, including an absent key,
// means the address is configured statically. The IPv6 client is separate and
// off unless DHCPV6C says otherwise.
func nsDHCPState(config map[string]string) (dhcp4, dhcp6 bool) {
switch strings.ToLower(config["BOOTPROTO"]) {
case "dhcp", "bootp":
dhcp4 = true
}
switch strings.ToLower(config["DHCPV6C"]) {
case "yes", "true", "1":
dhcp6 = true
}
return dhcp4, dhcp6
}
// nsSetDHCP writes the DHCP client state into an ifcfg config map. Disabling
// IPv4 leaves BOOTPROTO=none rather than deleting it: unlike the netplan and
// networkd schemas, an absent BOOTPROTO is read by some initscript versions as
// a prompt to guess, and "none" states the intent. PERSISTENT_DHCLIENT only has
// meaning while the IPv4 client runs, so it goes with it.
func nsSetDHCP(config map[string]string, dhcp4, dhcp6 bool) {
if dhcp4 {
config["BOOTPROTO"] = "dhcp"
} else {
config["BOOTPROTO"] = "none"
delete(config, "PERSISTENT_DHCLIENT")
}
if dhcp6 {
config["IPV6INIT"] = "yes"
config["DHCPV6C"] = "yes"
} else {
delete(config, "DHCPV6C")
}
}
// Either retreives an existing interface, or makes a new one.
func (ns *networkScripts) EnsureInterface(name string) *nsInterface {
// Find existing interface and return it.
for _, iface := range ns.Interfaces {
if iface.Name == name {
return iface
}
}
// Make a new interface as one wasn't found.
iface := &nsInterface{
Name: name,
}
ns.Interfaces = append(ns.Interfaces, iface)
return iface
}
// Parse a file that should only contain key=value data.
func parseKeyValueFile(fd *os.File) (map[string]string, error) {
// Setup the response.
resp := make(map[string]string)
var builder strings.Builder
readKey := false
inSingleQuote := false
inDoubleQuote := false
var key string
var value string
scanner := bufio.NewScanner(fd)
lineLoop:
for scanner.Scan() {
line := scanner.Bytes()
// Trim whitespace.
line = bytes.TrimFunc(line, unicode.IsSpace)
// Ignore blank lines.
if len(line) == 0 {
continue
}
// Ignore comments.
if line[0] == '#' {
continue
}
lineLen := len(line)
readLoop:
for i := 0; i < lineLen; i++ {
b := line[i]
// Handle single quotes.
if b == '\'' && !inDoubleQuote {
if inSingleQuote {
inSingleQuote = false
} else {
inSingleQuote = true
}
continue
}
// Handle double quotes.
if b == '"' && !inSingleQuote {
if inDoubleQuote {
inDoubleQuote = false
} else {
inDoubleQuote = true
}
continue
}
// Handle escapes.
if b == '\\' {
if i+1 >= lineLen {
continue lineLoop
}
if inDoubleQuote && line[i+1] == '"' {
i++
builder.WriteByte('"')
continue
}
}
// If equal sign and not in a quote, handle key logic.
if b == '=' && !inDoubleQuote && !inSingleQuote {
if readKey {
return nil, fmt.Errorf("invalid file syntax in %s", fd.Name())
}
key = builder.String()
builder.Reset()
readKey = true
continue
}
// Handle spaces outside of quotes.
if unicode.IsSpace(rune(b)) && !inDoubleQuote && !inSingleQuote {
// If there is an space outside of an quote,
// and its not leading to an comment or
// the end of line, it is invalid.
for ; i < lineLen; i++ {
b := line[i]
if unicode.IsSpace(rune(b)) {
continue
} else if b == '#' {
break readLoop
} else {
return nil, fmt.Errorf("invalid file syntax in %s", fd.Name())
}
}
break
}
// Write data.
builder.WriteByte(b)
}
// If we ended reading line in a quote, continue to next line.
if inDoubleQuote || inSingleQuote {
continue lineLoop
}
// If we didn't read a key, that's invalid.
if !readKey {
return nil, fmt.Errorf("invalid file syntax in %s", fd.Name())
}
// At this point, we have fully read the key/value.
value = builder.String()
builder.Reset()
readKey = false
// Assign key/value.
resp[key] = value
}
return resp, nil
}
// Read list of IP route formatted fields and return list of routes.
func parseIPRoutes(routes [][]string) (resp []*Route) {
// Loop through each route.
routeLoop:
for _, route := range routes {
// If the length of fields is 0, no route to parse.
routeLen := len(route)
if routeLen == 0 {
continue
}
// Try and parse the network field.
ip, network, err := net.ParseCIDR(route[0])
if err != nil {
continue
}
network.IP = ip
// Read the fields.
var gateway net.IP
metric := int(256)
for i := 1; i < routeLen; i++ {
field := route[i]
if field == "via" && i+1 < routeLen {
i++
gateway = net.ParseIP(route[i])
if gateway == nil {
continue routeLoop
}
}
if field == "metric" && i+1 < routeLen {
i++
metric, err = strconv.Atoi(route[i])
if err != nil || metric == 0 {
metric = 256
}
}
}
// Add the route.
r := new(Route)
r.Destination = network
r.Gateway = gateway
r.Metric = metric
resp = append(resp, r)
}
return
}
func newNetworkScripts(backupRetention int) (ns *networkScripts, err error) {
ns, err = newNetworkScriptsWithConfig(networkScriptsPath)
if ns != nil {
ns.backupRetention = backupRetention
}
return
}
func newNetworkScriptsWithConfig(configPath string) (ns *networkScripts, err error) {
// Try to read the directory files.
unsortedEntries, err := os.ReadDir(configPath)
if err != nil {
return
}
// Sort by name.
entries := sortableDirEntries(unsortedEntries)
sort.Sort(entries)
// Setup response.
ns = new(networkScripts)
ns.ConfigDir = configPath
// Setup regex match for route network/netmask style.
routeRe := regexp.MustCompile(`^\s*ADDRESS[0-9]+=`)
for _, entry := range entries {
// If not an network file, skip.
if entry.IsDir() {
continue
}
// If the file is a interface config file, read it.
if strings.HasPrefix(entry.Name(), "ifcfg-") {
// Open the file.
configFile := path.Join(configPath, entry.Name())
fd, err := os.Open(configFile)
if err != nil {
return nil, err
}
// Parse the file.
resp, err := parseKeyValueFile(fd)
fd.Close()
if err != nil {
return nil, err
}
// Find the device name.
devName, ok := resp["DEVICE"]
if !ok || devName == "" {
devName = entry.Name()[6:]
}
idx := strings.IndexByte(devName, ':')
if idx != -1 {
devName = devName[:idx]
}
// In the rare event the device name isn't found with the above,
// try hardware address matching.
if devName == "" {
hwAddr, ok := resp["HWADDR"]
mac, err := net.ParseMAC(hwAddr)
if !ok || err != nil {
return nil, fmt.Errorf("unable to find interface name in %s", configFile)
}
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
if bytes.Equal(iface.HardwareAddr, mac) {
devName = iface.Name
break
}
}
if devName == "" {
return nil, fmt.Errorf("unable to find interface name in %s", configFile)
}
}
// Setup file.
file := nsFile{
Name: entry.Name(),
Config: resp,
}
// Get the interface.
iface := ns.EnsureInterface(devName)
// Add the file to the config.
iface.IFFiles = append(iface.IFFiles, file)
}
// If the file is a route file.
if strings.HasPrefix(entry.Name(), "route-") {
// Get the device name for the network/netmask format.
devName := entry.Name()[6:]
if devName == "" {
return nil, fmt.Errorf("invalid route file name")
}
// Open the file.
configFile := path.Join(configPath, entry.Name())
fd, err := os.Open(configFile)
if err != nil {
return nil, err
}
// Confirm file type.
file := nsRouteFile{
Name: entry.Name(),
}
isNetworkMas := false
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
line := scanner.Text()
if routeRe.MatchString(line) {
isNetworkMas = true
break
}
}
fd.Seek(0, io.SeekStart)
// Parse depending on type.
if isNetworkMas {
// Parse key value file.
resp, err := parseKeyValueFile(fd)
fd.Close()
if err != nil {
return nil, err
}
// Parse network/netmask configuration.
for i := 0; ; i++ {
var route []string
addr, ok := resp[fmt.Sprintf("ADDRESS%d", i)]
if !ok {
break
}
netmask, ok := resp[fmt.Sprintf("NETMASK%d", i)]
if !ok {
return nil, fmt.Errorf("invalid file format, missing netmask %s", configFile)
}
gateway, ok := resp[fmt.Sprintf("GATEWAY%d", i)]
if !ok {
return nil, fmt.Errorf("invalid file format, missing gateway %s", configFile)
}
ipMask := net.ParseIP(netmask).To4()
if ipMask == nil {
return nil, fmt.Errorf("invalid file format %s", configFile)
}
mask := net.IPMask(ipMask)
prefix, _ := mask.Size()
route = append(route, fmt.Sprintf("%s/%d", addr, prefix), "via", gateway, "dev", devName)
file.Routes = append(file.Routes, route)
}
} else {
// Read ip command format routes.
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
// Ignore blank lines and comments.
if line == "" || line[0] == '#' {
continue
}
// Parse route.
route := strings.Fields(line)
file.Routes = append(file.Routes, route)
}
fd.Close()
}
// Get the interface.
iface := ns.EnsureInterface(devName)
// Add the route to the iface.
iface.Route4File = file
}
// If the file is a route file.
if strings.HasPrefix(entry.Name(), "route6-") {
// Get the device name for use in assigning to interface.
devName := entry.Name()[7:]
if devName == "" {
return nil, fmt.Errorf("invalid route file name")
}
// Open the file.
configFile := path.Join(configPath, entry.Name())
fd, err := os.Open(configFile)
if err != nil {
return nil, err
}
// Read ip command format routes.
file := nsRouteFile{
Name: entry.Name(),
}
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
// Ignore blank lines and comments.
if line == "" || line[0] == '#' {
continue
}
// Parse route.
route := strings.Fields(line)
file.Routes = append(file.Routes, route)
}
fd.Close()
// Get the interface.
iface := ns.EnsureInterface(devName)
// Add the route to the iface.
iface.Route6File = file
}
}
return
}
// Reads the environment variables at an IP index, and returns an ipnet.
func (ns *networkScripts) ParseIP(cfg map[string]string, idx string) *net.IPNet {
// Get the IPADDR environment, non-existing means no IP.
addr, ok := cfg["IPADDR"+idx]
if !ok || addr == "" {
return nil
}
// Get the prefix and netmask to get network size.
prefix := cfg["PREFIX"+idx]
netmask := cfg["NETMASK"+idx]
// If the prefix isn't found, try parsing it from the netmask.
if prefix == "" {
// Get the prefix from netmask, defaulting to 16.
ipMask := net.ParseIP(netmask).To4()
if ipMask != nil {
mask := net.IPMask(ipMask)
ones, _ := mask.Size()
prefix = strconv.Itoa(ones)
} else {
prefix = "16"
}
}
// Parse the CIDR.
ip, ipnet, err := net.ParseCIDR(fmt.Sprintf("%s/%s", addr, prefix))
if err != nil {
return nil
}
ipnet.IP = ip
return ipnet
}
// Get interfaces configured.
func (ns *networkScripts) GetInterfaces() (interfaces []*Interface, err error) {
// Connect to netlink.
var h *netlink.Handle
h, err = netlink.NewHandle()
if err != nil {
return
}
defer h.Close()
// Get list of interfaces to match with MAC address.
var links []netlink.Link
links, err = h.LinkList()
if err != nil {
return
}
for _, iface := range ns.Interfaces {
i := new(Interface)
i.Name = iface.Name
// Discover the interface name and mac address.
for _, link := range links {
if link.Attrs().Name == iface.Name {
i.MAC = link.Attrs().HardwareAddr
i.Link = link
}
}
// Parse interface files.
for _, file := range iface.IFFiles {
// Read the DHCP client state. Any of an interface's files enabling
// a client means the interface runs one.
dhcp4, dhcp6 := nsDHCPState(file.Config)
i.DHCP4 = i.DHCP4 || dhcp4
i.DHCP6 = i.DHCP6 || dhcp6
// Parse IPv4 addresses in the file.
ipAddr := ns.ParseIP(file.Config, "")
if ipAddr != nil {
i.Addresses = append(i.Addresses, ipAddr)
}
for idx := 0; idx <= 255; idx++ {
ipAddr = ns.ParseIP(file.Config, strconv.Itoa(idx))
if ipAddr != nil {
i.Addresses = append(i.Addresses, ipAddr)
} else if idx >= 2 {
break
}
}
gateway4 := file.Config["GATEWAY"]
if gateway4 != "" {
i.Gateway4 = net.ParseIP(gateway4)
}
gateway6 := file.Config["IPV6_DEFAULTGW"]
if gateway6 != "" {
i.Gateway6 = net.ParseIP(gateway6)
}
// Parse DNS servers (DNS1, DNS2, ...) and search domains.
for idx := 1; ; idx++ {
dns, ok := file.Config[fmt.Sprintf("DNS%d", idx)]
if !ok {
break
}
if ip := net.ParseIP(dns); ip != nil {
i.DNS = append(i.DNS, ip)
}
}
if domain := file.Config["DOMAIN"]; domain != "" {
i.SearchDomains = append(i.SearchDomains, strings.Fields(domain)...)
}
// Parse IPv6 addresses in the file.
addr := file.Config["IPV6ADDR"]
if addr != "" {
ip, ipnet, err := net.ParseCIDR(addr)
if err == nil {
ipnet.IP = ip
i.Addresses = append(i.Addresses, ipnet)
}
}
addrs := strings.Fields(file.Config["IPV6ADDR_SECONDARIES"])
for _, addr := range addrs {
ip, ipnet, err := net.ParseCIDR(addr)
if err == nil {
ipnet.IP = ip
i.Addresses = append(i.Addresses, ipnet)
}
}
}
// Parse static routes.
i.Routes = parseIPRoutes(iface.Route4File.Routes)
route6 := parseIPRoutes(iface.Route6File.Routes)
i.Routes = append(i.Routes, route6...)
// Add interface.
interfaces = append(interfaces, i)
}
return
}
// Save an interface to script.
func (n *nsInterface) Save(config map[string]string, configDir string, backupRetention int) error {
// Get config paths.
configName := fmt.Sprintf("ifcfg-%s", n.Name)
configPath := path.Join(configDir, configName)
tmpName := fmt.Sprintf(".tmp.%d.%s", time.Now().UnixNano(), configName)
tmpPath := path.Join(configDir, tmpName)
// Try to encode/save the config.
fd, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
var keys []string
for key := range config {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
val := config[key]
if strings.ContainsFunc(val, unicode.IsSpace) {
fmt.Fprintf(fd, "%s=\"%s\"\n", key, val)
} else {
fmt.Fprintf(fd, "%s=%s\n", key, val)
}
}
fd.Close()
// Backup existing configs.
prefix := fmt.Sprintf(".bak.%d.", time.Now().UnixNano())
for _, file := range n.IFFiles {
newName := prefix + file.Name
oldFile := path.Join(configDir, file.Name)
newFile := path.Join(configDir, newName)
err := fileMove(oldFile, newFile)
if err != nil {
return err
}
}
// Move new config in place.
err = fileMove(tmpPath, configPath)
if err != nil {
return err
}
// Update the config in the structure.
n.IFFiles = []nsFile{{
Name: configName,
Config: config,
}}
// Prune old backups beyond the retention count.
for _, file := range n.IFFiles {
pruneBackups(configDir, prefixBackupMatcher(file.Name), backupRetention)
}
return nil
}
// Save an interface to script.
func (n *nsInterface) SaveRouteFiles(routes4 []string, routes6 []string, configDir string, backupRetention int) error {
// Get config paths for routes IPv4.
config4Name := fmt.Sprintf("route-%s", n.Name)
config4Path := path.Join(configDir, config4Name)
tmp4Name := fmt.Sprintf(".tmp.%d.%s", time.Now().UnixNano(), config4Name)
tmp4Path := path.Join(configDir, tmp4Name)
// Try to encode/save the config.
if len(routes4) != 0 {
fd, err := os.OpenFile(tmp4Path, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
for _, route := range routes4 {
fmt.Fprintln(fd, route)
}
fd.Close()
}
// Get config paths for routes IPv6
config6Name := fmt.Sprintf("route6-%s", n.Name)
config6Path := path.Join(configDir, config6Name)
tmp6Name := fmt.Sprintf(".tmp.%d.%s", time.Now().UnixNano(), config6Name)
tmp6Path := path.Join(configDir, tmp6Name)
// Try to encode/save the config.
if len(routes6) != 0 {
fd, err := os.OpenFile(tmp6Path, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
for _, route := range routes6 {
fmt.Fprintf(fd, "%s\n", route)
}
fd.Close()
}
// Backup existing configs.
prefix := fmt.Sprintf(".bak.%d.", time.Now().UnixNano())
if n.Route4File.Name != "" {
newName := prefix + n.Route4File.Name
oldFile := path.Join(configDir, n.Route4File.Name)
newFile := path.Join(configDir, newName)
err := fileMove(oldFile, newFile)
if err != nil {
return err
}
}
if n.Route6File.Name != "" {
newName := prefix + n.Route6File.Name
oldFile := path.Join(configDir, n.Route6File.Name)
newFile := path.Join(configDir, newName)
err := fileMove(oldFile, newFile)
if err != nil {
return err
}
}
// Move new config in place.
if len(routes4) != 0 {
err := fileMove(tmp4Path, config4Path)
if err != nil {
return err
}
}
if len(routes6) != 0 {
err := fileMove(tmp6Path, config6Path)
if err != nil {
return err
}
}
// Update the config in the structure.
n.Route4File = nsRouteFile{}
if len(routes4) != 0 {
n.Route4File.Name = config4Name
for _, route := range routes4 {
n.Route4File.Routes = append(n.Route4File.Routes, strings.Fields(route))
}
}
n.Route6File = nsRouteFile{}
if len(routes6) != 0 {
n.Route6File.Name = config6Name
for _, route := range routes6 {
n.Route6File.Routes = append(n.Route6File.Routes, strings.Fields(route))
}
}
// Prune old backups beyond the retention count.
if n.Route4File.Name != "" {
pruneBackups(configDir, prefixBackupMatcher(n.Route4File.Name), backupRetention)
}
if n.Route6File.Name != "" {
pruneBackups(configDir, prefixBackupMatcher(n.Route6File.Name), backupRetention)
}
return nil
}
// Set addresses to interface.
func (ns *networkScripts) SetIfaceAddresses(_ context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) (err error) {
// Separate addresses by addr4 and addr6.
var addrs4 []*net.IPNet
var addrs6 []string
for _, addr := range addrs {
if addr.IP.To4() == nil {
addrs6 = append(addrs6, addr.String())
} else {
addrs4 = append(addrs4, addr)
}
}
// Find or create the interface. A backend must not silently no-op just
// because it has not seen this interface before (e.g. a freshly attached
// NIC), otherwise the caller gets a success with no persisted change.
ifc := ns.EnsureInterface(iface)
// Merge configs.
config := make(map[string]string)
for _, file := range ifc.IFFiles {
err = mergo.Merge(&config, file.Config)
if err != nil {
return err
}
}
// Delete existing configs.
delete(config, "IPADDR6")
delete(config, "IPV6ADDR")
delete(config, "IPV6ADDR_SECONDARIES")
delete(config, "IPV6_DEFAULTGW")
delete(config, "GATEWAY")
configMap := []string{""}
for i := 0; i <= 255; i++ {
configMap = append(configMap, strconv.Itoa(i))
}
for _, idx := range configMap {
delete(config, fmt.Sprintf("IPADDR%s", idx))
delete(config, fmt.Sprintf("PREFIX%s", idx))
delete(config, fmt.Sprintf("NETMASK%s", idx))
delete(config, fmt.Sprintf("BROADCAST%s", idx))
delete(config, fmt.Sprintf("ARPCHECK%s", idx))
delete(config, fmt.Sprintf("ARPUPDATE%s", idx))
}
// Add IP addresses.
for idx, ip := range addrs4 {
config[fmt.Sprintf("IPADDR%d", idx)] = ip.IP.String()
prefix, _ := ip.Mask.Size()
config[fmt.Sprintf("PREFIX%d", idx)] = strconv.Itoa(prefix)
config[fmt.Sprintf("NETMASK%d", idx)] = net.IP(ip.Mask).String()
config[fmt.Sprintf("BROADCAST%d", idx)] = getBroadcast(ip).String()
}
// Add IPv6 addresses.
if len(addrs6) != 0 {
config["IPV6ADDR"] = addrs6[0]
addrs6 = addrs6[1:]
if len(addrs6) != 0 {
config["IPV6ADDR_SECONDARIES"] = strings.Join(addrs6, " ")
}
}
// Set gateways.
if gateway4 != nil {
config["GATEWAY"] = gateway4.String()
}
if gateway6 != nil {
config["IPV6_DEFAULTGW"] = gateway6.String()
}
// Try to save.
err = ifc.Save(config, ns.ConfigDir, ns.backupRetention)
if err != nil {
return err
}
return
}
// Set the DHCP client state on an interface.
func (ns *networkScripts) SetIfaceDHCP(_ context.Context, iface string, dhcp4, dhcp6 bool) (err error) {
// Find or create the interface. A backend must not silently no-op just
// because it has not seen this interface before (e.g. a freshly attached
// NIC), otherwise the caller gets a success with no persisted change.
ifc := ns.EnsureInterface(iface)
// Merge configs.
config := make(map[string]string)
for _, file := range ifc.IFFiles {
err = mergo.Merge(&config, file.Config)
if err != nil {
return err
}
}
nsSetDHCP(config, dhcp4, dhcp6)
// Try to save.
return ifc.Save(config, ns.ConfigDir, ns.backupRetention)
}
// renewDHCP restarts the interface through the initscripts so the DHCP client
// that was just written to its ifcfg file is started. `ifup` on an interface
// that is already up is a no-op on these distributions, so it is taken down
// first; this is the same ifdown/ifup pair the initscripts themselves use, and
// it is what acquires the lease.
func (ns *networkScripts) renewDHCP(ctx context.Context, iface string) error {
// ifdown on an interface that is already down exits non-zero. That is not a
// failure of this call — the interface being down is the state ifup wants —
// so only the ifup result decides.
_, _ = runCommand(ctx, "ifdown", iface)
_, err := runCommand(ctx, "ifup", iface)
return err
}
// Set static routes to interface.
func (ns *networkScripts) SetIfaceRoutes(_ context.Context, iface string, routes []*Route) (err error) {
// Build route slices.
var routes4 []string
var routes6 []string
for _, route := range routes {
r := fmt.Sprintf("%s via %s metric %d", route.Destination.String(), route.Gateway, route.Metric)
if route.Destination.IP.To4() == nil {
routes6 = append(routes6, r)
} else {
routes4 = append(routes4, r)
}
}
// Find or create the interface. A backend must not silently no-op just
// because it has not seen this interface before (e.g. a freshly attached
// NIC), otherwise the caller gets a success with no persisted change.
ifc := ns.EnsureInterface(iface)
// Save the routes.
err = ifc.SaveRouteFiles(routes4, routes6, ns.ConfigDir, ns.backupRetention)
if err != nil {
return err
}
return
}
// Set DNS servers and search domains on interface.
func (ns *networkScripts) SetIfaceDNS(_ context.Context, iface string, servers []net.IP, searchDomains []string) (err error) {
// Find or create the interface. A backend must not silently no-op just
// because it has not seen this interface before (e.g. a freshly attached
// NIC), otherwise the caller gets a success with no persisted change.
ifc := ns.EnsureInterface(iface)
// Merge configs.
config := make(map[string]string)
for _, file := range ifc.IFFiles {
err = mergo.Merge(&config, file.Config)
if err != nil {
return err
}
}
// Delete existing DNS and search-domain configs. network-scripts
// stores resolvers as DNS1, DNS2, ... and the search list in DOMAIN.
for key := range config {
if strings.HasPrefix(key, "DNS") {
delete(config, key)
}
}
delete(config, "DOMAIN")
delete(config, "PEERDNS")
// Add DNS servers, numbered from 1 per the network-scripts format.
idx := 1
for _, ip := range servers {
if ip == nil {
continue
}
config[fmt.Sprintf("DNS%d", idx)] = ip.String()
idx++
}
// On a DHCP interface, dhclient rewrites /etc/resolv.conf from the
// lease on every renewal, which would override the DNS1/DNS2 set above.
// PEERDNS=no tells the initscripts not to accept DHCP-provided resolvers
// so the statically configured servers win. Only set it when we actually
// have servers to enforce; when clearing, leaving PEERDNS unset restores
// the default of honouring DHCP.
if idx > 1 {
config["PEERDNS"] = "no"
}
// Add search domains.
if len(searchDomains) != 0 {
config["DOMAIN"] = strings.Join(searchDomains, " ")
}
// Try to save.
err = ifc.Save(config, ns.ConfigDir, ns.backupRetention)
if err != nil {
return err
}
return
}