1095 lines
27 KiB
Go
1095 lines
27 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
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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 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.
|
|
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
|
|
}
|