Files
mgmt/engine/resources/net.go
James Shubin d30ff6cfae legal: Remove year
Instead of constantly making these updates, let's just remove the year
since things are stored in git anyways, and this is not an actual modern
legal risk anymore.
2025-01-26 16:24:51 -05:00

906 lines
26 KiB
Go

// Mgmt
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
//go:build !darwin
package resources
import (
"bytes"
"context"
"fmt"
"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/util"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/purpleidea/mgmt/util/recwatch"
"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. It supports flipping the state if you ask for it to be reversible.
type NetRes struct {
traits.Base // add the base methods without re-implementation
traits.Reversible
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
}
if obj.init.Debug {
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 := os.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 := os.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
}
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
// TODO: should this copy internal state?
func (obj *NetRes) Copy() engine.CopyableRes {
addrs := []string{}
for _, addr := range obj.Addrs {
addrs = append(addrs, addr)
}
var ipforward *bool
if obj.IPForward != nil { // copy the content, not the pointer...
b := *obj.IPForward
ipforward = &b
}
return &NetRes{
State: obj.State,
Addrs: addrs,
Gateway: obj.Gateway,
IPForward: ipforward,
}
}
// Reversed returns the "reverse" or "reciprocal" resource. This is used to
// "clean" up after a previously defined resource has been removed.
func (obj *NetRes) Reversed() (engine.ReversibleRes, error) {
cp, err := engine.ResCopy(obj)
if err != nil {
return nil, errwrap.Wrapf(err, "could not copy")
}
rev, ok := cp.(engine.ReversibleRes)
if !ok {
return nil, fmt.Errorf("not reversible")
}
rev.ReversibleMeta().Disabled = true // the reverse shouldn't run again
res, ok := cp.(*NetRes)
if !ok {
return nil, fmt.Errorf("copied res was not our kind")
}
// Only one field to reverse for now. It also removes the config file.
if obj.State == NetStateUp {
res.State = NetStateDown
}
if obj.State == NetStateDown {
res.State = NetStateUp
}
return res, 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
}