842 lines
24 KiB
Go
842 lines
24 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2023+ James Shubin and the project contributors
|
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
//go:build !darwin
|
|
|
|
package resources
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
|
|
"github.com/purpleidea/mgmt/engine"
|
|
"github.com/purpleidea/mgmt/engine/traits"
|
|
"github.com/purpleidea/mgmt/lang/funcs/vars"
|
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
|
"github.com/purpleidea/mgmt/lang/types"
|
|
"github.com/purpleidea/mgmt/recwatch"
|
|
"github.com/purpleidea/mgmt/util"
|
|
"github.com/purpleidea/mgmt/util/errwrap"
|
|
"github.com/purpleidea/mgmt/util/socketset"
|
|
|
|
// XXX: Do NOT use subscribe methods from this lib, as they are racey
|
|
// and do not clean up spawned goroutines. Should be replaced when a
|
|
// suitable alternative is available.
|
|
"github.com/vishvananda/netlink"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
func init() {
|
|
engine.RegisterResource(KindNet, func() engine.Res { return &NetRes{} })
|
|
|
|
// const.res.net.state.up = "up"
|
|
// const.res.net.state.down = "down"
|
|
vars.RegisterResourceParams(KindNet, map[string]map[string]func() interfaces.Var{
|
|
ParamNetState: {
|
|
NetStateUp: func() interfaces.Var {
|
|
return &types.StrValue{
|
|
V: NetStateUp,
|
|
}
|
|
},
|
|
NetStateDown: func() interfaces.Var {
|
|
return &types.StrValue{
|
|
V: NetStateDown,
|
|
}
|
|
},
|
|
// TODO: consider removing this field entirely
|
|
"undefined": func() interfaces.Var {
|
|
return &types.StrValue{
|
|
V: NetStateUndefined, // empty string
|
|
}
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
const (
|
|
// KindNet is the kind string used to identify this resource.
|
|
KindNet = "net"
|
|
|
|
// ParamNetState is the name of the state field parameter.
|
|
ParamNetState = "state"
|
|
|
|
// NetStateUp is the string that represents that the net state should be
|
|
// up. This is the on interface state.
|
|
NetStateUp = "up"
|
|
|
|
// NetStateDown is the string that represents that the net state should
|
|
// be down. This is the off interface state.
|
|
NetStateDown = "down"
|
|
|
|
// NetStateUndefined means the net state has not been specified.
|
|
// TODO: consider moving to *string and express this state as a nil.
|
|
NetStateUndefined = ""
|
|
|
|
// IfacePrefix is the prefix used to identify unit files for managed
|
|
// links.
|
|
IfacePrefix = "mgmt-"
|
|
|
|
// networkdUnitFileDir is the location of networkd unit files which
|
|
// define the systemd network connections.
|
|
networkdUnitFileDir = "/etc/systemd/network/"
|
|
|
|
// networkdUnitFileExt is the file extension for networkd unit files.
|
|
networkdUnitFileExt = ".network"
|
|
|
|
// networkdUnitFileUmask sets the permissions on the systemd unit file.
|
|
networkdUnitFileUmask = 0644
|
|
|
|
// Netlink multicast groups to watch for events. For all groups see:
|
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
|
|
rtmGrps = rtmGrpLink | rtmGrpIPv4IfAddr | rtmGrpIPv6IfAddr | rtmGrpIPv4IfRoute
|
|
rtmGrpLink = 0x1 // interface create/delete/up/down
|
|
rtmGrpIPv4IfAddr = 0x10 // add/delete IPv4 addresses
|
|
rtmGrpIPv6IfAddr = 0x100 // add/delete IPv6 addresses
|
|
rtmGrpIPv4IfRoute = 0x40 // add delete routes
|
|
|
|
// IP routing protocols for used for netlink route messages. For all
|
|
// protocols see:
|
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
|
|
rtProtoKernel = 2 // kernel
|
|
rtProtoStatic = 4 // static
|
|
|
|
socketFile = "pipe.sock" // path in vardir to store our socket file
|
|
)
|
|
|
|
// NetRes is a network interface resource based on netlink. It manages the state
|
|
// of a network link. Configuration is also stored in a networkd configuration
|
|
// file, so the network is available upon reboot. The name of the resource is
|
|
// the string representing the network interface name. This could be "eth0" for
|
|
// example.
|
|
type NetRes struct {
|
|
traits.Base // add the base methods without re-implementation
|
|
|
|
init *engine.Init
|
|
|
|
// State is the desired state of the interface. It can be "up", "down",
|
|
// or the empty string to leave that unspecified.
|
|
State string `lang:"state" yaml:"state"`
|
|
|
|
// Addrs is the list of addresses to set on the interface. They must
|
|
// each be in CIDR notation such as: 192.0.2.42/24 for example.
|
|
Addrs []string `lang:"addrs" yaml:"addrs"`
|
|
|
|
// Gateway represents the default route to set for the interface.
|
|
Gateway string `lang:"gateway" yaml:"gateway"`
|
|
|
|
// IPForward is a boolean that sets whether we should forward incoming
|
|
// packets onward when this is set. It default to unspecified, which
|
|
// downstream (in the systemd-networkd configuration) defaults to false.
|
|
// XXX: this could also be "ipv4" or "ipv6", add those as a second option?
|
|
IPForward *bool `lang:"ip_forward" yaml:"ip_forward"`
|
|
|
|
iface *iface // a struct containing the net.Interface and netlink.Link
|
|
unitFilePath string // the interface unit file path
|
|
|
|
socketFile string // path for storing the pipe socket file
|
|
}
|
|
|
|
// nlChanStruct defines the channel used to send netlink messages and errors to
|
|
// the event processing loop in Watch.
|
|
type nlChanStruct struct {
|
|
msg []syscall.NetlinkMessage
|
|
err error
|
|
}
|
|
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *NetRes) Default() engine.Res {
|
|
return &NetRes{}
|
|
}
|
|
|
|
// Validate if the params passed in are valid data.
|
|
func (obj *NetRes) Validate() error {
|
|
// validate state
|
|
if obj.State != NetStateUp && obj.State != NetStateDown && obj.State != "" {
|
|
return fmt.Errorf("state must be up, down or empty")
|
|
}
|
|
|
|
// validate network address input
|
|
if obj.Addrs != nil {
|
|
for i, addr := range obj.Addrs {
|
|
if _, _, err := net.ParseCIDR(addr); err != nil {
|
|
if len(obj.Addrs) == 1 {
|
|
return errwrap.Wrapf(err, "error parsing addr")
|
|
}
|
|
return errwrap.Wrapf(err, "error parsing addrs[%d]", i)
|
|
}
|
|
}
|
|
}
|
|
if obj.Gateway != "" {
|
|
if g := net.ParseIP(obj.Gateway); g == nil {
|
|
return fmt.Errorf("error parsing gateway: %s", obj.Gateway)
|
|
}
|
|
}
|
|
|
|
// validate the interface name
|
|
_, err := net.InterfaceByName(obj.Name())
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Init runs some startup code for this resource.
|
|
func (obj *NetRes) Init(init *engine.Init) error {
|
|
obj.init = init // save for later
|
|
|
|
var err error
|
|
|
|
// tmp directory for pipe socket
|
|
dir, err := obj.init.VarDir("")
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
|
}
|
|
obj.socketFile = path.Join(dir, socketFile) // return a unique file
|
|
|
|
// store the network interface in the struct
|
|
obj.iface = &iface{}
|
|
if obj.iface.iface, err = net.InterfaceByName(obj.Name()); err != nil {
|
|
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
|
|
}
|
|
// store the netlink link to use as interface input in netlink functions
|
|
if obj.iface.link, err = netlink.LinkByName(obj.Name()); err != nil {
|
|
return errwrap.Wrapf(err, "error finding link: %s", obj.Name())
|
|
}
|
|
|
|
// build the path to the networkd configuration file
|
|
obj.unitFilePath = networkdUnitFileDir + IfacePrefix + obj.Name() + networkdUnitFileExt
|
|
|
|
return nil
|
|
}
|
|
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *NetRes) Cleanup() error {
|
|
if obj.socketFile == "/" {
|
|
return fmt.Errorf("socket file should not be the root path")
|
|
}
|
|
if obj.socketFile != "" { // safety
|
|
if err := os.Remove(obj.socketFile); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Watch listens for events from the specified interface via a netlink socket.
|
|
// TODO: currently gets events from ALL interfaces, would be nice to reject
|
|
// events from other interfaces.
|
|
func (obj *NetRes) Watch(ctx context.Context) error {
|
|
// create a netlink socket for receiving network interface events
|
|
conn, err := socketset.NewSocketSet(rtmGrps, obj.socketFile, unix.NETLINK_ROUTE)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error creating socket set")
|
|
}
|
|
|
|
// waitgroup for netlink receive goroutine
|
|
wg := &sync.WaitGroup{}
|
|
defer conn.Close()
|
|
// We must wait for the Shutdown() AND the select inside of SocketSet to
|
|
// complete before we Close, since the unblocking in SocketSet is not a
|
|
// synchronous operation.
|
|
defer wg.Wait()
|
|
defer conn.Shutdown() // close the netlink socket and unblock conn.receive()
|
|
|
|
// watch the systemd-networkd configuration file
|
|
recWatcher, err := recwatch.NewRecWatcher(obj.unitFilePath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// close the recwatcher when we're done
|
|
defer recWatcher.Close()
|
|
|
|
// channel for netlink messages
|
|
nlChan := make(chan *nlChanStruct) // closed from goroutine
|
|
|
|
// channel to unblock selects in goroutine
|
|
closeChan := make(chan struct{})
|
|
defer close(closeChan)
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer close(nlChan)
|
|
for {
|
|
// receive messages from the socket set
|
|
msgs, err := conn.ReceiveNetlinkMessages()
|
|
if err != nil {
|
|
select {
|
|
case nlChan <- &nlChanStruct{
|
|
err: errwrap.Wrapf(err, "error receiving messages"),
|
|
}:
|
|
case <-closeChan:
|
|
return
|
|
}
|
|
}
|
|
select {
|
|
case nlChan <- &nlChanStruct{
|
|
msg: msgs,
|
|
}:
|
|
case <-closeChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
|
|
var send = false // send event?
|
|
var done bool
|
|
for {
|
|
select {
|
|
case s, ok := <-nlChan:
|
|
if !ok {
|
|
if done {
|
|
return nil
|
|
}
|
|
done = true
|
|
continue
|
|
}
|
|
if err := s.err; err != nil {
|
|
return errwrap.Wrapf(s.err, "unknown netlink error")
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Event: %+v", s.msg)
|
|
}
|
|
|
|
send = true
|
|
|
|
case event, ok := <-recWatcher.Events():
|
|
if !ok {
|
|
if done {
|
|
return nil
|
|
}
|
|
done = true
|
|
continue
|
|
}
|
|
if err := event.Error; err != nil {
|
|
return errwrap.Wrapf(err, "unknown recwatcher error")
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
|
}
|
|
|
|
send = true
|
|
|
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
|
return nil
|
|
}
|
|
|
|
// do all our event sending all together to avoid duplicate msgs
|
|
if send {
|
|
send = false
|
|
obj.init.Event() // notify engine of an event (this can block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ifaceCheckApply checks the state of the network device and brings it up or
|
|
// down as necessary.
|
|
func (obj *NetRes) ifaceCheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// check the interface state
|
|
state, err := obj.iface.state()
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error checking %s state", obj.Name())
|
|
}
|
|
// if the state is correct or unspecified, we're done
|
|
if obj.State == state || obj.State == "" {
|
|
return true, nil
|
|
}
|
|
|
|
// end of state checking
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
obj.init.Logf("ifaceCheckApply(%t)", apply)
|
|
|
|
// ip link set up/down
|
|
if err := obj.iface.linkUpDown(obj.State); err != nil {
|
|
return false, errwrap.Wrapf(err, "error setting %s up or down", obj.Name())
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// addrCheckApply checks if the interface has the correct addresses and then
|
|
// adds/deletes addresses as necessary.
|
|
func (obj *NetRes) addrCheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// get the link's addresses
|
|
ifaceAddrs, err := obj.iface.getAddrs()
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error getting addresses from %s", obj.Name())
|
|
}
|
|
// if state is not defined
|
|
if obj.Addrs == nil {
|
|
// send addrs
|
|
obj.Addrs = ifaceAddrs
|
|
return true, nil
|
|
}
|
|
// check if all addrs have a kernel route needed for first hop
|
|
kernelOK, err := obj.iface.kernelCheck(obj.Addrs)
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error checking kernel routes")
|
|
}
|
|
|
|
// if the kernel routes are intact and the addrs match, we're done
|
|
err = util.SortedStrSliceCompare(obj.Addrs, ifaceAddrs)
|
|
if err == nil && kernelOK {
|
|
return true, nil
|
|
}
|
|
|
|
// end of state checking
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
obj.init.Logf("addrCheckApply(%t)", apply)
|
|
|
|
// check each address and delete the ones that aren't in the definition
|
|
if err := obj.iface.addrApplyDelete(obj.Addrs); err != nil {
|
|
return false, errwrap.Wrapf(err, "error checking or deleting addresses")
|
|
}
|
|
// check each address and add the ones that are defined but do not exist
|
|
if err := obj.iface.addrApplyAdd(obj.Addrs); err != nil {
|
|
return false, errwrap.Wrapf(err, "error checking or adding addresses")
|
|
}
|
|
// make sure all the addrs have the appropriate kernel routes
|
|
if err := obj.iface.kernelApply(obj.Addrs); err != nil {
|
|
return false, errwrap.Wrapf(err, "error adding kernel routes")
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// gatewayCheckApply checks if the interface has the correct default gateway and
|
|
// adds/deletes routes as necessary.
|
|
func (obj *NetRes) gatewayCheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// get all routes from the interface
|
|
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error getting default routes")
|
|
}
|
|
// add default routes to a slice
|
|
defRoutes := []netlink.Route{}
|
|
for _, route := range routes {
|
|
if route.Dst == nil { // route is default
|
|
defRoutes = append(defRoutes, route)
|
|
}
|
|
}
|
|
// if the gateway is already set, we're done
|
|
if len(defRoutes) == 1 && defRoutes[0].Gw.String() == obj.Gateway {
|
|
return true, nil
|
|
}
|
|
// if no gateway was defined
|
|
if obj.Gateway == "" {
|
|
// send the gateway if there is one
|
|
if len(defRoutes) == 1 {
|
|
obj.Gateway = defRoutes[0].Gw.String()
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// end of state checking
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
obj.init.Logf("gatewayCheckApply(%t)", apply)
|
|
|
|
// delete all but one default route
|
|
for i := 1; i < len(defRoutes); i++ {
|
|
if err := netlink.RouteDel(&defRoutes[i]); err != nil {
|
|
return false, errwrap.Wrapf(err, "error deleting route: %+v", defRoutes[i])
|
|
}
|
|
}
|
|
|
|
// add or change the default route
|
|
if err := netlink.RouteReplace(&netlink.Route{
|
|
LinkIndex: obj.iface.iface.Index,
|
|
Gw: net.ParseIP(obj.Gateway),
|
|
Protocol: rtProtoStatic,
|
|
}); err != nil {
|
|
return false, errwrap.Wrapf(err, "error replacing default route")
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// fileCheckApply checks and maintains the systemd-networkd unit file contents.
|
|
// TODO: replace this with a file resource if we can do so cleanly.
|
|
func (obj *NetRes) fileCheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
// if the state is unspecified, we're done
|
|
if obj.State == "" {
|
|
return true, nil
|
|
}
|
|
|
|
// check if the unit file exists
|
|
_, err := os.Stat(obj.unitFilePath)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return false, errwrap.Wrapf(err, "error checking file")
|
|
}
|
|
|
|
exists := err == nil
|
|
if obj.State == NetStateDown && !exists {
|
|
return true, nil
|
|
}
|
|
|
|
fileContents := []byte{}
|
|
if exists {
|
|
// check the file contents
|
|
byt, err := ioutil.ReadFile(obj.unitFilePath)
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error reading file")
|
|
}
|
|
fileContents = byt
|
|
}
|
|
// build the unit file contents from the definition
|
|
contents := obj.unitFileContents()
|
|
|
|
if obj.State == NetStateUp && exists && bytes.Equal(fileContents, contents) {
|
|
return true, nil
|
|
}
|
|
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
obj.init.Logf("fileCheckApply(%t)", apply)
|
|
|
|
if obj.State == NetStateDown && exists {
|
|
if err := os.Remove(obj.unitFilePath); err != nil {
|
|
return false, errwrap.Wrapf(err, "error removing configuration file")
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// all other situations, we write the file
|
|
if err := ioutil.WriteFile(obj.unitFilePath, contents, networkdUnitFileUmask); err != nil {
|
|
return false, errwrap.Wrapf(err, "error writing configuration file")
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// CheckApply is run to check the state and, if apply is true, to apply the
|
|
// necessary changes to reach the desired state. This is run before Watch and
|
|
// again if Watch finds a change occurring to the state.
|
|
func (obj *NetRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
checkOK := true
|
|
|
|
// check the networkd unit file
|
|
if c, err := obj.fileCheckApply(ctx, apply); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
|
|
// check the network device
|
|
if c, err := obj.ifaceCheckApply(ctx, apply); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
|
|
// if the interface is supposed to be down, we're done
|
|
if obj.State == NetStateDown {
|
|
return checkOK, nil
|
|
}
|
|
|
|
// check the addresses
|
|
if c, err := obj.addrCheckApply(ctx, apply); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
|
|
// check the gateway
|
|
if c, err := obj.gatewayCheckApply(ctx, apply); err != nil {
|
|
return false, err
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
|
|
return checkOK, nil
|
|
}
|
|
|
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
|
func (obj *NetRes) Cmp(r engine.Res) error {
|
|
// we can only compare NetRes to others of the same resource kind
|
|
res, ok := r.(*NetRes)
|
|
if !ok {
|
|
return fmt.Errorf("not a %s", obj.Kind())
|
|
}
|
|
|
|
if obj.State != res.State {
|
|
return fmt.Errorf("the State differs")
|
|
}
|
|
if (obj.Addrs == nil) != (res.Addrs == nil) {
|
|
return fmt.Errorf("the Addrs differ")
|
|
}
|
|
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
|
|
return fmt.Errorf("the Addrs differ")
|
|
}
|
|
if obj.Gateway != res.Gateway {
|
|
return fmt.Errorf("the Gateway differs")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NetUID is a unique resource identifier.
|
|
type NetUID struct {
|
|
// NOTE: There is also a name variable in the BaseUID struct, this is
|
|
// information about where this UID came from, and is unrelated to the
|
|
// information about the resource we're matching. That data which is
|
|
// used in the IFF function, is what you see in the struct fields here.
|
|
engine.BaseUID
|
|
|
|
name string // the network interface name
|
|
}
|
|
|
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
|
func (obj *NetUID) IFF(uid engine.ResUID) bool {
|
|
res, ok := uid.(*NetUID)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return obj.name == res.name
|
|
}
|
|
|
|
// UIDs includes all params to make a unique identification of this object. Most
|
|
// resources only return one although some resources can return multiple.
|
|
func (obj *NetRes) UIDs() []engine.ResUID {
|
|
x := &NetUID{
|
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
|
name: obj.Name(),
|
|
}
|
|
return []engine.ResUID{x}
|
|
}
|
|
|
|
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes NetRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*NetRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to NetRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = NetRes(raw) // restore from indirection with type conversion!
|
|
return nil
|
|
}
|
|
|
|
// unitFileContents builds the unit file contents from the definition.
|
|
func (obj *NetRes) unitFileContents() []byte {
|
|
// build the unit file contents
|
|
u := []string{"[Match]"}
|
|
u = append(u, fmt.Sprintf("Name=%s", obj.Name()))
|
|
u = append(u, "[Network]")
|
|
for _, addr := range obj.Addrs {
|
|
u = append(u, fmt.Sprintf("Address=%s", addr))
|
|
}
|
|
if obj.Gateway != "" {
|
|
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
|
|
}
|
|
if obj.IPForward != nil {
|
|
b := "false"
|
|
if *obj.IPForward {
|
|
b = "true"
|
|
}
|
|
u = append(u, fmt.Sprintf("IPForward=%s", b))
|
|
}
|
|
c := strings.Join(u, "\n")
|
|
return []byte(c)
|
|
}
|
|
|
|
// iface wraps net.Interface to add additional methods.
|
|
type iface struct {
|
|
iface *net.Interface
|
|
link netlink.Link
|
|
}
|
|
|
|
// state reports the state of the interface as up or down.
|
|
func (obj *iface) state() (string, error) {
|
|
var err error
|
|
if obj.iface, err = net.InterfaceByName(obj.iface.Name); err != nil {
|
|
return "", errwrap.Wrapf(err, "error updating interface")
|
|
}
|
|
// if the interface's "up" flag is 0, it's down
|
|
if obj.iface.Flags&net.FlagUp == 0 {
|
|
return NetStateDown, nil
|
|
}
|
|
// otherwise it's up
|
|
return NetStateUp, nil
|
|
}
|
|
|
|
// linkUpDown brings the interface up or down, depending on input value.
|
|
func (obj *iface) linkUpDown(state string) error {
|
|
if state != NetStateUp && state != NetStateDown {
|
|
return fmt.Errorf("state must be up or down")
|
|
}
|
|
if state == NetStateUp {
|
|
return netlink.LinkSetUp(obj.link)
|
|
}
|
|
return netlink.LinkSetDown(obj.link)
|
|
}
|
|
|
|
// getAddrs returns a list of strings containing all of the interface's IP
|
|
// addresses in CIDR format.
|
|
func (obj *iface) getAddrs() ([]string, error) {
|
|
var ifaceAddrs []string
|
|
a, err := obj.iface.Addrs()
|
|
if err != nil {
|
|
return nil, errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
|
}
|
|
// we're only interested in the strings (not the network)
|
|
for _, addr := range a {
|
|
ifaceAddrs = append(ifaceAddrs, addr.String())
|
|
}
|
|
return ifaceAddrs, nil
|
|
}
|
|
|
|
// kernelCheck checks if all addresses in the list have a corresponding kernel
|
|
// route, without which the network would be unreachable.
|
|
func (obj *iface) kernelCheck(addrs []string) (bool, error) {
|
|
var routeOK bool
|
|
|
|
// get a list of all the routes associated with the interface
|
|
routes, err := netlink.RouteList(obj.link, netlink.FAMILY_V4)
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error getting routes")
|
|
}
|
|
// check each route against each addr
|
|
for i, addr := range addrs {
|
|
routeOK = false
|
|
ip, ipNet, err := net.ParseCIDR(addr)
|
|
if err != nil {
|
|
if len(addrs) == 1 {
|
|
return false, errwrap.Wrapf(err, "error parsing addr")
|
|
}
|
|
return false, errwrap.Wrapf(err, "error parsing addrs[%d]", i)
|
|
}
|
|
for _, r := range routes {
|
|
// if src, dst and protocol are correct, the kernel route exists
|
|
if r.Src.Equal(ip) && r.Dst.String() == ipNet.String() && r.Protocol == rtProtoKernel {
|
|
routeOK = true
|
|
break
|
|
}
|
|
}
|
|
// if any addr is missing a kernel route return early
|
|
if !routeOK {
|
|
break
|
|
}
|
|
}
|
|
return routeOK, nil
|
|
}
|
|
|
|
// kernelApply adds or replaces each address' kernel route as necessary.
|
|
func (obj *iface) kernelApply(addrs []string) error {
|
|
// for each addr, add or replace the corresponding kernel route
|
|
for i, addr := range addrs {
|
|
ip, ipNet, err := net.ParseCIDR(addr)
|
|
if err != nil {
|
|
if len(addrs) == 1 {
|
|
return errwrap.Wrapf(err, "error parsing addr")
|
|
}
|
|
return errwrap.Wrapf(err, "error parsing addrs[%d]", i)
|
|
}
|
|
// kernel route needed for the network to be reachable from a given ip
|
|
if err := netlink.RouteReplace(&netlink.Route{
|
|
LinkIndex: obj.iface.Index,
|
|
Dst: ipNet,
|
|
Src: ip,
|
|
Protocol: rtProtoKernel,
|
|
Scope: netlink.SCOPE_LINK,
|
|
}); err != nil {
|
|
return errwrap.Wrapf(err, "error replacing first hop route")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// addrApplyDelete, checks the interface's addresses and deletes any that are
|
|
// not in the list/definition.
|
|
func (obj *iface) addrApplyDelete(objAddrs []string) error {
|
|
ifaceAddrs, err := obj.getAddrs()
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
|
}
|
|
for _, ifaceAddr := range ifaceAddrs {
|
|
addrOK := false
|
|
for _, objAddr := range objAddrs {
|
|
if ifaceAddr == objAddr {
|
|
addrOK = true
|
|
}
|
|
}
|
|
if addrOK {
|
|
continue
|
|
}
|
|
addr, err := netlink.ParseAddr(ifaceAddr)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error parsing netlink address: %s", ifaceAddr)
|
|
}
|
|
if err := netlink.AddrDel(obj.link, addr); err != nil {
|
|
return errwrap.Wrapf(err, "error deleting addr: %s from %s", ifaceAddr, obj.iface.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// addrApplyAdd checks if the interface has each address in the supplied list,
|
|
// and if it doesn't, it adds them.
|
|
func (obj *iface) addrApplyAdd(objAddrs []string) error {
|
|
ifaceAddrs, err := obj.getAddrs()
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
|
}
|
|
for _, objAddr := range objAddrs {
|
|
addrOK := false
|
|
for _, ifaceAddr := range ifaceAddrs {
|
|
if ifaceAddr == objAddr {
|
|
addrOK = true
|
|
}
|
|
}
|
|
if addrOK {
|
|
continue
|
|
}
|
|
addr, err := netlink.ParseAddr(objAddr)
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "error parsing cidr address: %s", objAddr)
|
|
}
|
|
if err := netlink.AddrAdd(obj.link, addr); err != nil {
|
|
return errwrap.Wrapf(err, "error adding addr: %s to %s", objAddr, obj.iface.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|