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".
1224 lines
32 KiB
Go
1224 lines
32 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/vishvananda/netlink"
|
|
)
|
|
|
|
const ifUpDownConfig = "/etc/network/interfaces"
|
|
|
|
type ifInterface struct {
|
|
Name string
|
|
Mode string
|
|
isDHCP bool
|
|
isPPP bool
|
|
Files []string
|
|
Addresses []*net.IPNet
|
|
Gateway4 net.IP
|
|
Gateway6 net.IP
|
|
Routes []*Route
|
|
Config map[string][]string
|
|
}
|
|
|
|
type ifUpDown struct {
|
|
Interfaces []*ifInterface
|
|
BaseConfig string
|
|
BackupDir string
|
|
backupRetention int
|
|
}
|
|
|
|
// stanzaFamily reports the address family this interface's stanza configures.
|
|
// "iface eth0 inet dhcp" configures IPv4, "iface eth0 inet6 static" IPv6. An
|
|
// interface this backend has not seen has no stanza and reports "".
|
|
func (iface *ifInterface) stanzaFamily() string {
|
|
fields := strings.Fields(iface.Mode)
|
|
if len(fields) == 0 {
|
|
return ""
|
|
}
|
|
switch fields[0] {
|
|
case "inet", "inet6":
|
|
return fields[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// dhcpState reports whether each family's DHCP client is enabled. A stanza only
|
|
// ever configures one family, so at most one of these is ever true.
|
|
func (iface *ifInterface) dhcpState() (dhcp4, dhcp6 bool) {
|
|
if !iface.isDHCP {
|
|
return false, false
|
|
}
|
|
family := iface.stanzaFamily()
|
|
return family == "inet", family == "inet6"
|
|
}
|
|
|
|
// setDHCP switches the stanza's method between "dhcp" and "static" for the
|
|
// family the stanza configures.
|
|
//
|
|
// Unlike netplan or networkd, an ifupdown interface is a set of stanzas and
|
|
// each names exactly one family, so this backend cannot express "DHCPv4 on,
|
|
// DHCPv6 on" from the single stanza it tracks per interface. Asking it to
|
|
// enable DHCP for the family its stanza does not configure is therefore an
|
|
// error rather than a silent no-op — the caller would otherwise be told the
|
|
// request succeeded while nothing was written. Disabling that other family is
|
|
// already true of a stanza that never configured it, so it is accepted.
|
|
func (iface *ifInterface) setDHCP(dhcp4, dhcp6 bool) error {
|
|
if iface.isPPP {
|
|
return fmt.Errorf("cannot change DHCP on PPP interface")
|
|
}
|
|
|
|
// An interface with no stanza yet gets one for the family being enabled.
|
|
family := iface.stanzaFamily()
|
|
if family == "" {
|
|
switch {
|
|
case dhcp4 && dhcp6:
|
|
return fmt.Errorf("ifupdown cannot enable DHCP for both families on one stanza")
|
|
case dhcp4:
|
|
iface.Mode = "inet dhcp"
|
|
iface.isDHCP = true
|
|
case dhcp6:
|
|
iface.Mode = "inet6 dhcp"
|
|
iface.isDHCP = true
|
|
default:
|
|
iface.Mode = "inet static"
|
|
iface.isDHCP = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Split the request into the family this stanza speaks for and the one it
|
|
// does not. Only the former can be honored.
|
|
enable, other := dhcp4, dhcp6
|
|
if family == "inet6" {
|
|
enable, other = dhcp6, dhcp4
|
|
}
|
|
if other {
|
|
return fmt.Errorf("ifupdown cannot enable DHCP for the other family on an %q stanza for %s", family, iface.Name)
|
|
}
|
|
|
|
fields := strings.Fields(iface.Mode)
|
|
method := ""
|
|
if len(fields) > 1 {
|
|
method = fields[1]
|
|
}
|
|
switch method {
|
|
case "loopback", "ppp", "tunnel", "wvdial":
|
|
return fmt.Errorf("cannot change DHCP on %q interface %s", method, iface.Name)
|
|
}
|
|
|
|
switch {
|
|
case enable && method != "dhcp":
|
|
iface.Mode = family + " dhcp"
|
|
iface.isDHCP = true
|
|
case !enable && method == "dhcp":
|
|
iface.Mode = family + " static"
|
|
iface.isDHCP = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Either retreives an existing interface, or makes a new one.
|
|
func (i *ifUpDown) EnsureInterface(name string) *ifInterface {
|
|
// Find existing interface and return it.
|
|
for _, iface := range i.Interfaces {
|
|
if iface.Name == name {
|
|
return iface
|
|
}
|
|
}
|
|
|
|
// Make a new interface as one wasn't found. Seed it with the base config
|
|
// file so Save (which iterates iface.Files) actually persists the new
|
|
// block; without a file the interface would be held only in memory and
|
|
// silently dropped on write.
|
|
iface := &ifInterface{
|
|
Name: name,
|
|
Files: []string{i.BaseConfig},
|
|
Config: make(map[string][]string),
|
|
}
|
|
i.Interfaces = append(i.Interfaces, iface)
|
|
return iface
|
|
}
|
|
|
|
// Parses an ifupdown interface file.
|
|
func (i *ifUpDown) ReadFile(configFile string) error {
|
|
// Open the config file.
|
|
fd, err := os.Open(configFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fd.Close()
|
|
|
|
// Parse the file.
|
|
var iface *ifInterface
|
|
var address *net.IPNet
|
|
scanner := bufio.NewScanner(fd)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
// Get index of comments and remove them.
|
|
idx := strings.IndexByte(line, '#')
|
|
if idx != -1 {
|
|
line = line[:idx]
|
|
}
|
|
|
|
// Trim whitespace.
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Ignore blank lines.
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Parse fields.
|
|
fields := strings.Fields(line)
|
|
switch fields[0] {
|
|
// For the global configs, we just skip as we are not reconstructing
|
|
// the entire file when saving changes.
|
|
case "auto", "allow-hotplug":
|
|
continue
|
|
|
|
// On source directory, read all files in the specified directory.
|
|
case "source-directory":
|
|
// If no directory, return error.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid source directory syntax")
|
|
}
|
|
|
|
// Try to read the directory files.
|
|
unsortedEntries, err := os.ReadDir(fields[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sort by name.
|
|
entries := sortableDirEntries(unsortedEntries)
|
|
sort.Sort(entries)
|
|
|
|
// Read each file.
|
|
for _, entry := range entries {
|
|
// Skip directories.
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
filePath := path.Join(fields[1], entry.Name())
|
|
err := i.ReadFile(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// On source, read file specified.
|
|
case "source":
|
|
// If no file specified, error.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid source file syntax")
|
|
}
|
|
|
|
// Read the file specified.
|
|
err := i.ReadFile(fields[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// For interface configs, set current interface and parse type.
|
|
case "iface", "interface":
|
|
// We require at least the interface name.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid interface syntax")
|
|
}
|
|
|
|
// Finalize any addresses.
|
|
if address != nil {
|
|
iface.Addresses = append(iface.Addresses, address)
|
|
address = nil
|
|
}
|
|
|
|
// Ensure interface is created.
|
|
ifname := fields[1]
|
|
idx := strings.IndexByte(ifname, ':')
|
|
if idx != -1 {
|
|
ifname = ifname[:idx]
|
|
}
|
|
iface = i.EnsureInterface(ifname)
|
|
|
|
// Track the file the interface has configurations in. A
|
|
// dual-stack interface has separate "inet"/"inet6" stanzas that
|
|
// commonly live in the same file, so only record a file once per
|
|
// interface: Save processes iface.Files by index using tmp/backup
|
|
// names computed a single time up front, and reprocessing the
|
|
// same path a second time would clobber the backup of the true
|
|
// original with the already-rewritten intermediate content.
|
|
if !stringInSlice(iface.Files, configFile) {
|
|
iface.Files = append(iface.Files, configFile)
|
|
}
|
|
|
|
// Determine type.
|
|
for _, field := range fields[2:] {
|
|
if field == "dhcp" {
|
|
iface.isDHCP = true
|
|
} else if field == "ppp" {
|
|
iface.isPPP = true
|
|
}
|
|
}
|
|
|
|
// Save mode for cases where we need to write.
|
|
iface.Mode = strings.Join(fields[2:], " ")
|
|
|
|
// Parse assigned addresses.
|
|
case "address":
|
|
// We require an address field.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid address syntax")
|
|
}
|
|
|
|
// If no interface, fail.
|
|
if iface == nil {
|
|
return fmt.Errorf("no interface to assign address to")
|
|
}
|
|
|
|
// If an address to be parsed is there, add to the list of addresses.
|
|
if address != nil {
|
|
iface.Addresses = append(iface.Addresses, address)
|
|
address = nil
|
|
}
|
|
|
|
// If not cider, parse IP.
|
|
if !strings.Contains(fields[1], "/") {
|
|
address = &net.IPNet{
|
|
IP: net.ParseIP(fields[1]),
|
|
}
|
|
if address.IP == nil {
|
|
return fmt.Errorf("invalid IP provided: %s", fields[1])
|
|
}
|
|
if address.IP.To4() == nil {
|
|
address.Mask = net.CIDRMask(128, 128)
|
|
} else {
|
|
address.Mask = net.CIDRMask(32, 32)
|
|
}
|
|
} else {
|
|
var ip net.IP
|
|
ip, address, err = net.ParseCIDR(fields[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
address.IP = ip
|
|
}
|
|
|
|
// If an netmask is set, assign to the last parsed address.
|
|
case "netmask":
|
|
// We require an netmask field.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid netmask syntax")
|
|
}
|
|
|
|
// Require an address.
|
|
if address == nil {
|
|
return fmt.Errorf("no address to apply netmask")
|
|
}
|
|
|
|
// If the netmask is an ipmask, parse that.
|
|
if strings.Contains(fields[1], ".") {
|
|
ipMask := net.ParseIP(fields[1]).To4()
|
|
if ipMask == nil {
|
|
return fmt.Errorf("invalid netmask: %s", fields[1])
|
|
}
|
|
address.Mask = net.IPMask(ipMask)
|
|
} else {
|
|
// This is just the prefix.
|
|
prefix, err := strconv.Atoi(fields[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if address.IP.To4() == nil {
|
|
address.Mask = net.CIDRMask(prefix, 128)
|
|
} else {
|
|
address.Mask = net.CIDRMask(prefix, 32)
|
|
}
|
|
}
|
|
|
|
// If an gateway is defined, assign according to family.
|
|
case "gateway":
|
|
// We require an gateway field.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid gateway syntax")
|
|
}
|
|
|
|
// Parse the gateway.
|
|
gateway := net.ParseIP(fields[1])
|
|
if gateway == nil {
|
|
return fmt.Errorf("failed to parse gateway: %s", fields[1])
|
|
}
|
|
|
|
// Add the gateway by family.
|
|
if gateway.To4() == nil {
|
|
iface.Gateway6 = gateway
|
|
} else {
|
|
iface.Gateway4 = gateway
|
|
}
|
|
|
|
// For other configurations, save to the interface key value config.
|
|
default:
|
|
// All field sshould have key and value.
|
|
if len(fields) < 2 {
|
|
return fmt.Errorf("invalid syntax")
|
|
}
|
|
|
|
// If no interface, skip.
|
|
if iface == nil {
|
|
continue
|
|
}
|
|
|
|
// If up script, try parsing fields.
|
|
if fields[0] == "up" && len(fields) >= 4 {
|
|
// If the route command is called, see if we can parse
|
|
if fields[1] == "route" {
|
|
var dst *net.IPNet
|
|
var gateway net.IP
|
|
metric := int(256)
|
|
isAdd := false
|
|
fieldLen := len(fields)
|
|
for f := 2; f < fieldLen; f++ {
|
|
field := fields[f]
|
|
if f+1 >= fieldLen {
|
|
continue
|
|
}
|
|
switch field {
|
|
case "add":
|
|
isAdd = true
|
|
case "-net", "-host":
|
|
continue
|
|
case "netmask":
|
|
f++
|
|
// A netmask before any destination is malformed; skip
|
|
// it rather than dereferencing a nil dst.
|
|
if dst == nil {
|
|
continue
|
|
}
|
|
ipMask := net.ParseIP(fields[f]).To4()
|
|
if ipMask == nil {
|
|
continue
|
|
}
|
|
dst.Mask = net.IPMask(ipMask)
|
|
case "gw":
|
|
f++
|
|
gateway = net.ParseIP(fields[f])
|
|
case "metric":
|
|
f++
|
|
metric, err = strconv.Atoi(fields[f])
|
|
if err != nil {
|
|
metric = 256
|
|
}
|
|
default:
|
|
// If no destination parsed, check if we can parse one here.
|
|
if dst != nil {
|
|
continue
|
|
}
|
|
// The destination is the current field (e.g. the token
|
|
// after "-net"/"-host"), not the next one.
|
|
host := fields[f]
|
|
if strings.Contains(host, "/") {
|
|
ip, ipnet, err := net.ParseCIDR(host)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ipnet.IP = ip
|
|
dst = ipnet
|
|
} else {
|
|
dst = &net.IPNet{
|
|
IP: net.ParseIP(host),
|
|
}
|
|
if dst.IP == nil {
|
|
dst = nil
|
|
continue
|
|
}
|
|
if dst.IP.To4() == nil {
|
|
dst.Mask = net.CIDRMask(128, 128)
|
|
} else {
|
|
dst.Mask = net.CIDRMask(32, 32)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found that we're adding and all needed
|
|
// fields are parsed. Add route and skip adding
|
|
// key value config.
|
|
if isAdd && dst != nil && gateway != nil {
|
|
r := new(Route)
|
|
r.Destination = dst
|
|
r.Gateway = gateway
|
|
r.Metric = metric
|
|
iface.Routes = append(iface.Routes, r)
|
|
continue
|
|
}
|
|
|
|
// On the ip command, this can either be an route
|
|
// or an address.
|
|
} else if fields[1] == "ip" {
|
|
isRoute := false
|
|
isAddr := false
|
|
isAdd := false
|
|
thisDev := false
|
|
var address *net.IPNet
|
|
var dst *net.IPNet
|
|
var gateway net.IP
|
|
metric := int(256)
|
|
fieldLen := len(fields)
|
|
for f := 2; f < fieldLen; f++ {
|
|
field := fields[f]
|
|
if f+1 >= fieldLen {
|
|
continue
|
|
}
|
|
if field == "addr" || field == "address" {
|
|
isAddr = true
|
|
} else if field == "route" {
|
|
isRoute = true
|
|
} else if field == "add" {
|
|
// typically the address field for both
|
|
// routes and addresses is right after
|
|
// the add command.
|
|
isAdd = true
|
|
f++
|
|
ip, network, err := net.ParseCIDR(fields[f])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
network.IP = ip
|
|
if isAddr {
|
|
address = network
|
|
} else if isRoute {
|
|
dst = network
|
|
}
|
|
} else if field == "dev" {
|
|
f++
|
|
devName := fields[f]
|
|
if devName == "$IFACE" {
|
|
thisDev = true
|
|
} else if devName == iface.Name {
|
|
thisDev = true
|
|
}
|
|
} else if field == "via" {
|
|
f++
|
|
gateway = net.ParseIP(fields[f])
|
|
} else if field == "metric" {
|
|
f++
|
|
metric, err = strconv.Atoi(fields[f])
|
|
if err != nil || metric == 0 {
|
|
metric = 256
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we parsed an add command, add parsed info.
|
|
if isAdd {
|
|
// If valid route or address, add and skip
|
|
// adding the key/value config.
|
|
if isRoute && dst != nil && gateway != nil {
|
|
r := new(Route)
|
|
r.Destination = dst
|
|
r.Gateway = gateway
|
|
r.Metric = metric
|
|
iface.Routes = append(iface.Routes, r)
|
|
continue
|
|
} else if isAddr && address != nil && thisDev {
|
|
iface.Addresses = append(iface.Addresses, address)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if the down script can be skipped due to route or address.
|
|
if fields[0] == "down" && len(fields) >= 4 {
|
|
// Parse route del commands to see if its removing a route
|
|
// that was added.
|
|
if fields[1] == "route" {
|
|
var dst *net.IPNet
|
|
isDel := false
|
|
fieldLen := len(fields)
|
|
for f := 2; f < fieldLen; f++ {
|
|
field := fields[f]
|
|
if f+1 >= fieldLen {
|
|
continue
|
|
}
|
|
switch field {
|
|
case "del":
|
|
isDel = true
|
|
case "-net", "-host":
|
|
continue
|
|
case "netmask":
|
|
f++
|
|
// A netmask before any destination is malformed; skip
|
|
// it rather than dereferencing a nil dst.
|
|
if dst == nil {
|
|
continue
|
|
}
|
|
ipMask := net.ParseIP(fields[f]).To4()
|
|
if ipMask == nil {
|
|
continue
|
|
}
|
|
dst.Mask = net.IPMask(ipMask)
|
|
default:
|
|
// If no destination parsed, check if we can parse one here.
|
|
if dst != nil {
|
|
continue
|
|
}
|
|
// The destination is the current field (e.g. the token
|
|
// after "-net"/"-host"), not the next one.
|
|
host := fields[f]
|
|
if strings.Contains(host, "/") {
|
|
ip, ipnet, err := net.ParseCIDR(host)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ipnet.IP = ip
|
|
dst = ipnet
|
|
} else {
|
|
dst = &net.IPNet{
|
|
IP: net.ParseIP(host),
|
|
}
|
|
if dst.IP == nil {
|
|
dst = nil
|
|
continue
|
|
}
|
|
if dst.IP.To4() == nil {
|
|
dst.Mask = net.CIDRMask(128, 128)
|
|
} else {
|
|
dst.Mask = net.CIDRMask(32, 32)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If route del and we parsed the desitnation,
|
|
// confirm the destination is being added and
|
|
// ignore this key/value add if it is.
|
|
if isDel && dst != nil {
|
|
exists := false
|
|
for _, route := range iface.Routes {
|
|
if route.Destination.Contains(dst.IP) {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if exists {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// On IP del command, parse and determine if we should
|
|
// skip adding key/value.
|
|
} else if fields[1] == "ip" {
|
|
isRoute := false
|
|
isAddr := false
|
|
isDel := false
|
|
thisDev := false
|
|
var address *net.IPNet
|
|
var dst *net.IPNet
|
|
fieldLen := len(fields)
|
|
for f := 2; f < fieldLen; f++ {
|
|
field := fields[f]
|
|
if f+1 >= fieldLen {
|
|
continue
|
|
}
|
|
if field == "addr" || field == "address" {
|
|
isAddr = true
|
|
} else if field == "route" {
|
|
isRoute = true
|
|
} else if field == "del" || field == "delete" {
|
|
isDel = true
|
|
f++
|
|
ip, network, err := net.ParseCIDR(fields[f])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
network.IP = ip
|
|
if isAddr {
|
|
address = network
|
|
} else if isRoute {
|
|
dst = network
|
|
}
|
|
} else if field == "dev" {
|
|
f++
|
|
devName := fields[f]
|
|
if devName == "$IFACE" {
|
|
thisDev = true
|
|
} else if devName == iface.Name {
|
|
thisDev = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// If this is an del and the parsed information exists
|
|
// in the appropiate bucket. Skip adding key/value.
|
|
if isDel {
|
|
if isRoute && dst != nil {
|
|
continue
|
|
} else if isAddr && address != nil && thisDev {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the name and value.
|
|
name := fields[0]
|
|
value := strings.Join(fields[1:], " ")
|
|
|
|
// Add config.
|
|
config := iface.Config[name]
|
|
config = append(config, value)
|
|
iface.Config[name] = config
|
|
}
|
|
}
|
|
|
|
// Finalize any addresses.
|
|
if address != nil {
|
|
iface.Addresses = append(iface.Addresses, address)
|
|
address = nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Setup an ifupdown config tool.
|
|
func newIfUpDown(backupRetention int) (i *ifUpDown, err error) {
|
|
i = new(ifUpDown)
|
|
i.backupRetention = backupRetention
|
|
i.BaseConfig = ifUpDownConfig
|
|
i.BackupDir = ifUpDownConfig + ".backup"
|
|
err = i.ReadFile(i.BaseConfig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get interfaces configured.
|
|
func (i *ifUpDown) 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 i.Interfaces {
|
|
i := new(Interface)
|
|
i.Name = iface.Name
|
|
i.DHCP4, i.DHCP6 = iface.dhcpState()
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Add addresses.
|
|
for _, address := range iface.Addresses {
|
|
i.Addresses = append(i.Addresses, address)
|
|
}
|
|
|
|
// Add gateways.
|
|
i.Gateway4 = iface.Gateway4
|
|
i.Gateway6 = iface.Gateway6
|
|
|
|
// Add DNS servers and search domains from the resolvconf stanzas.
|
|
for _, val := range iface.Config["dns-nameservers"] {
|
|
for _, field := range strings.Fields(val) {
|
|
if ip := net.ParseIP(field); ip != nil {
|
|
i.DNS = append(i.DNS, ip)
|
|
}
|
|
}
|
|
}
|
|
for _, val := range iface.Config["dns-search"] {
|
|
i.SearchDomains = append(i.SearchDomains, strings.Fields(val)...)
|
|
}
|
|
|
|
// Add static routes.
|
|
i.Routes = iface.Routes
|
|
|
|
// Add interface.
|
|
interfaces = append(interfaces, i)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Write our interface config to the specified file.
|
|
func (iface *ifInterface) Write(fd *os.File) error {
|
|
// Write the interface config.
|
|
if iface.Mode != "" {
|
|
fmt.Fprintf(fd, "iface %s %s\n", iface.Name, iface.Mode)
|
|
} else {
|
|
fmt.Fprintf(fd, "iface %s\n", iface.Name)
|
|
}
|
|
|
|
// Add first address of each family.
|
|
first4 := true
|
|
first6 := true
|
|
for _, addr := range iface.Addresses {
|
|
if addr.IP.To4() == nil {
|
|
if !first6 {
|
|
continue
|
|
}
|
|
first6 = false
|
|
fmt.Fprintf(fd, "\taddress %s\n", addr.String())
|
|
} else {
|
|
if !first4 {
|
|
continue
|
|
}
|
|
first4 = false
|
|
fmt.Fprintf(fd, "\taddress %s\n", addr.String())
|
|
}
|
|
}
|
|
|
|
// Add gateways.
|
|
if iface.Gateway4 != nil {
|
|
fmt.Fprintf(fd, "\tgateway %s\n", iface.Gateway4.String())
|
|
}
|
|
if iface.Gateway6 != nil {
|
|
fmt.Fprintf(fd, "\tgateway %s\n", iface.Gateway6.String())
|
|
}
|
|
|
|
// Add key/value configs.
|
|
var keys []string
|
|
hasUp := false
|
|
hasDown := false
|
|
for key := range iface.Config {
|
|
if key == "up" {
|
|
hasUp = true
|
|
continue
|
|
}
|
|
if key == "down" {
|
|
hasDown = true
|
|
continue
|
|
}
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
if hasUp {
|
|
keys = append(keys, "up")
|
|
}
|
|
if hasDown {
|
|
keys = append(keys, "down")
|
|
}
|
|
for _, key := range keys {
|
|
values := iface.Config[key]
|
|
for _, value := range values {
|
|
fmt.Fprintf(fd, "\t%s %s\n", key, value)
|
|
}
|
|
}
|
|
|
|
// Add any additional IP addresses via the ip command.
|
|
first4 = false
|
|
first6 = false
|
|
for _, addr := range iface.Addresses {
|
|
if addr.IP.To4() == nil {
|
|
if !first6 {
|
|
first6 = true
|
|
continue
|
|
}
|
|
fmt.Fprintf(fd, "\tup ip -6 addr add %s dev $IFACE\n", addr.String())
|
|
fmt.Fprintf(fd, "\tdown ip -6 addr del %s dev $IFACE\n", addr.String())
|
|
} else {
|
|
if !first4 {
|
|
first4 = true
|
|
continue
|
|
}
|
|
fmt.Fprintf(fd, "\tup ip addr add %s dev $IFACE\n", addr.String())
|
|
fmt.Fprintf(fd, "\tdown ip addr del %s dev $IFACE\n", addr.String())
|
|
}
|
|
}
|
|
|
|
// Add static routes.
|
|
for _, route := range iface.Routes {
|
|
if route.Destination.IP.To4() == nil {
|
|
fmt.Fprintf(fd, "\tup ip -6 route add %s via %s metric %d dev $IFACE\n", route.Destination.String(), route.Gateway.String(), route.Metric)
|
|
fmt.Fprintf(fd, "\tdown ip -6 route del %s via %s metric %d dev $IFACE\n", route.Destination.String(), route.Gateway.String(), route.Metric)
|
|
} else {
|
|
fmt.Fprintf(fd, "\tup ip route add %s via %s metric %d dev $IFACE\n", route.Destination.String(), route.Gateway.String(), route.Metric)
|
|
fmt.Fprintf(fd, "\tdown ip route del %s via %s metric %d dev $IFACE\n", route.Destination.String(), route.Gateway.String(), route.Metric)
|
|
}
|
|
}
|
|
fmt.Fprintln(fd)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Save the interface config to the files read.
|
|
func (iface *ifInterface) Save(backupDir string, backupRetention int) error {
|
|
// Confirm the backup dir is created.
|
|
if _, serr := os.Stat(backupDir); os.IsNotExist(serr) {
|
|
err := os.Mkdir(backupDir, 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Setup prefix and for each file the interface was found in,
|
|
// update the files.
|
|
prefixTmp := fmt.Sprintf(".tmp.%d.", time.Now().UnixNano())
|
|
prefixBak := fmt.Sprintf(".bak.%d.", time.Now().UnixNano())
|
|
for i, filePath := range iface.Files {
|
|
// Get temporary file path.
|
|
file := path.Base(filePath)
|
|
tmp := prefixTmp + file
|
|
tmpPath := path.Join(backupDir, tmp)
|
|
bak := prefixBak + file
|
|
bakPath := path.Join(backupDir, bak)
|
|
|
|
// Open tmp file for writing.
|
|
fdw, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the config file exists.
|
|
fileExists := true
|
|
if _, serr := os.Stat(filePath); os.IsNotExist(serr) {
|
|
fileExists = false
|
|
}
|
|
|
|
// If the file doesn't exist, then we're not trying to replace
|
|
// the interface config in a file of potential other interface configs.
|
|
if !fileExists {
|
|
// Only write our configs on the first file.
|
|
if i == 0 {
|
|
fmt.Fprintf(fdw, "auto %s\n", iface.Name)
|
|
fmt.Fprintf(fdw, "allow-hotplug %s\n\n", iface.Name)
|
|
iface.Write(fdw)
|
|
}
|
|
} else {
|
|
// The file does exist, so we should read the original file
|
|
// and try to pull out the original interface config ignoring
|
|
// other interface configs.
|
|
|
|
// Open file for reading.
|
|
fd, err := os.Open(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Scan original file.
|
|
scanner := bufio.NewScanner(fd)
|
|
foundIface := false
|
|
wroteIface := false
|
|
blankLineCount := 0
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
origLine := line
|
|
|
|
// Get index of comments and remove them.
|
|
idx := strings.IndexByte(line, '#')
|
|
comment := false
|
|
if idx != -1 {
|
|
comment = true
|
|
line = line[:idx]
|
|
}
|
|
|
|
// Trim whitespace.
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Ignore blank lines, writing them back to the file.
|
|
if len(line) == 0 {
|
|
// If the blank line is actually a comment line,
|
|
// we do not want to apply the blank line limit.
|
|
if comment {
|
|
fmt.Fprintln(fdw, origLine)
|
|
blankLineCount = 0
|
|
continue
|
|
}
|
|
|
|
// Increment the count of blank lines, allowing 1
|
|
// spacing apart.
|
|
blankLineCount++
|
|
if blankLineCount <= 1 {
|
|
fmt.Fprintln(fdw, origLine)
|
|
}
|
|
continue
|
|
} else {
|
|
// Reset the count as this isn't a blank line.
|
|
blankLineCount = 0
|
|
}
|
|
|
|
// Parse fields.
|
|
fields := strings.Fields(line)
|
|
switch fields[0] {
|
|
// On the global configs, check if the current interface
|
|
// is in its list of interfaces. If it is not, we should
|
|
// ensure we write the config and continue to the next
|
|
// interface.
|
|
case "auto", "allow-hotplug":
|
|
containsIface := false
|
|
for _, ifname := range fields[1:] {
|
|
idx := strings.IndexByte(ifname, ':')
|
|
if idx != -1 {
|
|
ifname = ifname[:idx]
|
|
}
|
|
if iface.Name == ifname {
|
|
containsIface = true
|
|
}
|
|
}
|
|
if containsIface && foundIface && !wroteIface {
|
|
wroteIface = true
|
|
foundIface = false
|
|
if i == 0 {
|
|
err = iface.Write(fdw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
fmt.Fprintln(fdw, origLine)
|
|
|
|
// On the interface config, check if its this interface
|
|
// so we can determine when to write our configs.
|
|
// If its not this interface, we will just write the config
|
|
// line for line.
|
|
case "iface", "interface":
|
|
// We require at least the interface name.
|
|
if len(fields) < 2 {
|
|
fmt.Fprintln(fdw, origLine)
|
|
continue
|
|
}
|
|
|
|
// Check if this is the interface.
|
|
ifname := fields[1]
|
|
idx := strings.IndexByte(ifname, ':')
|
|
if idx != -1 {
|
|
ifname = ifname[:idx]
|
|
}
|
|
if iface.Name == ifname {
|
|
foundIface = true
|
|
} else if foundIface && !wroteIface {
|
|
wroteIface = true
|
|
foundIface = false
|
|
if i == 0 {
|
|
err = iface.Write(fdw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if !foundIface {
|
|
fmt.Fprintln(fdw, origLine)
|
|
}
|
|
|
|
// On default, its a regular config which we will skip
|
|
// when we're on our interface, but write line by line for
|
|
// other interfaces.
|
|
default:
|
|
if foundIface {
|
|
continue
|
|
}
|
|
fmt.Fprintln(fdw, origLine)
|
|
}
|
|
}
|
|
|
|
// if we did not write the interface, write it.
|
|
if !wroteIface {
|
|
err = iface.Write(fdw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Close the open file.
|
|
fd.Close()
|
|
}
|
|
|
|
// We're done writing, so let's close files.
|
|
fdw.Close()
|
|
|
|
// Backup existing config.
|
|
if fileExists {
|
|
err = fileMove(filePath, bakPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Move new config in place.
|
|
err = fileMove(tmpPath, filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prune old backups of this config file beyond the retention count.
|
|
pruneBackups(backupDir, prefixBackupMatcher(file), backupRetention)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set the DHCP client state on an interface.
|
|
func (i *ifUpDown) 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 := i.EnsureInterface(iface)
|
|
|
|
// Switching to DHCP hands addressing to the lease, so the static addresses
|
|
// and gateways the stanza carried are dropped rather than written back
|
|
// under a "dhcp" method that has no address option to hold them.
|
|
dhcp := dhcp4 || dhcp6
|
|
if err = ifc.setDHCP(dhcp4, dhcp6); err != nil {
|
|
return err
|
|
}
|
|
if dhcp {
|
|
ifc.Addresses = nil
|
|
ifc.Gateway4 = nil
|
|
ifc.Gateway6 = nil
|
|
}
|
|
|
|
// Save the interface config.
|
|
return ifc.Save(i.BackupDir, i.backupRetention)
|
|
}
|
|
|
|
// renewDHCP restarts the interface through ifupdown so the "dhcp" method that
|
|
// was just written to its stanza is acted on and a lease is acquired. ifup on
|
|
// an already-up interface is a no-op, so it is taken down first.
|
|
func (i *ifUpDown) renewDHCP(ctx context.Context, iface string) error {
|
|
// ifdown on an interface that is already down exits non-zero, which is not
|
|
// a failure of this call — down is the state ifup wants it in.
|
|
_, _ = runCommand(ctx, "ifdown", iface)
|
|
_, err := runCommand(ctx, "ifup", iface)
|
|
return err
|
|
}
|
|
|
|
// Set addresses to interface.
|
|
func (i *ifUpDown) SetIfaceAddresses(_ context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) (err error) {
|
|
// Find the interface.
|
|
for _, ifc := range i.Interfaces {
|
|
if ifc.Name != iface {
|
|
continue
|
|
}
|
|
|
|
// Ensure we can adjust addresses. An ifupdown "dhcp" stanza takes no
|
|
// address option, so unlike netplan or networkd this backend cannot
|
|
// carry a static address alongside a lease on the same stanza. Call
|
|
// SetIfaceDHCP to convert the stanza to static first.
|
|
if ifc.isDHCP {
|
|
return fmt.Errorf("cannot change addresses on DHCP interface")
|
|
} else if ifc.isPPP {
|
|
return fmt.Errorf("cannot change addresses on PPP interface")
|
|
}
|
|
|
|
// Set provided addresses.
|
|
ifc.Addresses = addrs
|
|
ifc.Gateway4 = gateway4
|
|
ifc.Gateway6 = gateway6
|
|
|
|
// Save the interface config.
|
|
err = ifc.Save(i.BackupDir, i.backupRetention)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set static routes to interface.
|
|
func (i *ifUpDown) SetIfaceRoutes(_ context.Context, iface string, routes []*Route) (err error) {
|
|
// Find the interface.
|
|
for _, ifc := range i.Interfaces {
|
|
if ifc.Name != iface {
|
|
continue
|
|
}
|
|
|
|
// Ensure we can adjust static routes.
|
|
if ifc.isDHCP {
|
|
return fmt.Errorf("cannot change static routes on DHCP interface")
|
|
} else if ifc.isPPP {
|
|
return fmt.Errorf("cannot change static routes on PPP interface")
|
|
}
|
|
|
|
// Set provided static routes.
|
|
ifc.Routes = routes
|
|
|
|
// Save the interface config.
|
|
err = ifc.Save(i.BackupDir, i.backupRetention)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Set DNS servers and search domains on interface. ifupdown relies on the
|
|
// resolvconf integration, which reads the dns-nameservers and dns-search
|
|
// stanzas from the interface block.
|
|
func (i *ifUpDown) 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 := i.EnsureInterface(iface)
|
|
|
|
// Ensure we can adjust configuration.
|
|
if ifc.isDHCP {
|
|
return fmt.Errorf("cannot change DNS on DHCP interface")
|
|
} else if ifc.isPPP {
|
|
return fmt.Errorf("cannot change DNS on PPP interface")
|
|
}
|
|
|
|
// Replace the DNS stanzas.
|
|
delete(ifc.Config, "dns-nameservers")
|
|
delete(ifc.Config, "dns-search")
|
|
dns := ipStrings(servers)
|
|
if len(dns) != 0 {
|
|
ifc.Config["dns-nameservers"] = []string{strings.Join(dns, " ")}
|
|
}
|
|
if len(searchDomains) != 0 {
|
|
ifc.Config["dns-search"] = []string{strings.Join(searchDomains, " ")}
|
|
}
|
|
|
|
// The dns-nameservers/dns-search stanzas are not understood by ifup
|
|
// itself; they are consumed by the resolvconf integration hook. On a host
|
|
// without the resolvconf package installed (common on stock Ubuntu 14.04
|
|
// servers) these stanzas are inert and the persisted DNS never reaches
|
|
// /etc/resolv.conf on boot. Warn so the operator knows the write alone is
|
|
// not sufficient.
|
|
if (len(dns) != 0 || len(searchDomains) != 0) && !resolvconfInstalled() {
|
|
logger.Printf("IfUpDown: resolvconf not found; dns-nameservers/dns-search in %s will not take effect on reboot without the resolvconf package", ifUpDownConfig)
|
|
}
|
|
|
|
// Save the interface config.
|
|
err = ifc.Save(i.BackupDir, i.backupRetention)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// resolvconfInstalled reports whether the resolvconf binary is available on
|
|
// PATH, which the ifupdown dns-* stanzas depend on to update the live resolver.
|
|
func resolvconfInstalled() bool {
|
|
_, err := exec.LookPath("resolvconf")
|
|
return err == nil
|
|
}
|