blob: a1f06bcfd47ceb4da52b20863f6ce30858c79e8c [file] [log] [blame]
Lorenz Brun9601f262020-12-09 19:44:41 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17// Package callback contains minimal callbacks for configuring the kernel with options received over DHCP.
18//
19// These directly configure the relevant kernel subsytems and need to own certain parts of them as documented on a per-
20// callback basis to make sure that they can recover from restarts and crashes of the DHCP client.
21// The callbacks in here are not suitable for use in advanced network scenarios like running multiple DHCP clients
22// per interface via ClientIdentifier or when running an external FIB manager. In these cases it's advised to extract
23// the necessary information from the lease in your own callback and communicate it directly to the responsible entity.
24package callback
25
26import (
27 "fmt"
28 "math"
29 "net"
30 "os"
31 "time"
32
33 "git.monogon.dev/source/nexantic.git/core/pkg/dhcp4c"
34
35 "github.com/insomniacslk/dhcp/dhcpv4"
36 "github.com/vishvananda/netlink"
37 "golang.org/x/sys/unix"
38)
39
40// Compose can be used to chain multiple callbacks
41func Compose(callbacks ...dhcp4c.LeaseCallback) dhcp4c.LeaseCallback {
42 return func(old, new *dhcp4c.Lease) error {
43 for _, cb := range callbacks {
44 if err := cb(old, new); err != nil {
45 return err
46 }
47 }
48 return nil
49 }
50}
51
52func isIPNetEqual(a, b *net.IPNet) bool {
53 if a == b {
54 return true
55 }
56 if a == nil || b == nil {
57 return false
58 }
59 aOnes, aBits := a.Mask.Size()
60 bOnes, bBits := b.Mask.Size()
61 return a.IP.Equal(b.IP) && aOnes == bOnes && aBits == bBits
62}
63
64// ManageIP sets up and tears down the assigned IP address. It takes exclusive ownership of all IPv4 addresses
65// on the given interface which do not have IFA_F_PERMANENT set, so it's not possible to run multiple dynamic addressing
66// clients on a single interface.
67func ManageIP(iface netlink.Link) dhcp4c.LeaseCallback {
68 return func(old, new *dhcp4c.Lease) error {
69 newNet := new.IPNet()
70
71 addrs, err := netlink.AddrList(iface, netlink.FAMILY_V4)
72 if err != nil {
73 return fmt.Errorf("netlink failed to list addresses: %w", err)
74 }
75
76 for _, addr := range addrs {
77 if addr.Flags&unix.IFA_F_PERMANENT == 0 {
78 // Linux identifies addreses by IP, mask and peer (see net/ipv4/devinet.find_matching_ifa in Linux 5.10)
79 // So don't touch addresses which match on these properties as AddrReplace will atomically reconfigure
80 // them anyways without interrupting things.
81 if isIPNetEqual(addr.IPNet, newNet) && addr.Peer == nil && new != nil {
82 continue
83 }
84
85 if err := netlink.AddrDel(iface, &addr); !os.IsNotExist(err) && err != nil {
86 return fmt.Errorf("failed to delete address: %w", err)
87 }
88 }
89 }
90
91 if new != nil {
92 remainingLifetimeSecs := int(math.Ceil(new.ExpiresAt.Sub(time.Now()).Seconds()))
93 newBroadcastIP := dhcpv4.GetIP(dhcpv4.OptionBroadcastAddress, new.Options)
94 if err := netlink.AddrReplace(iface, &netlink.Addr{
95 IPNet: newNet,
96 ValidLft: remainingLifetimeSecs,
97 PreferedLft: remainingLifetimeSecs,
98 Broadcast: newBroadcastIP,
99 }); err != nil {
100 return fmt.Errorf("failed to update address: %w", err)
101 }
102 }
103 return nil
104 }
105}
106
107// ManageDefaultRoute manages a default route through the first router offered by DHCP. It does nothing if DHCP
108// doesn't provide any routers. It takes ownership of all RTPROTO_DHCP routes on the given interface, so it's not
109// possible to run multiple DHCP clients on the given interface.
110func ManageDefaultRoute(iface netlink.Link) dhcp4c.LeaseCallback {
111 return func(old, new *dhcp4c.Lease) error {
112 newRouter := new.Router()
113
114 dhcpRoutes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{
115 Protocol: unix.RTPROT_DHCP,
116 LinkIndex: iface.Attrs().Index,
117 }, netlink.RT_FILTER_OIF|netlink.RT_FILTER_PROTOCOL)
118 if err != nil {
119 return fmt.Errorf("netlink failed to list routes: %w", err)
120 }
121 ipv4DefaultRoute := net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}
122 for _, route := range dhcpRoutes {
123 // Don't remove routes which can be atomically replaced by RouteReplace to prevent potential traffic
124 // disruptions.
125 if !isIPNetEqual(&ipv4DefaultRoute, route.Dst) && newRouter != nil {
126 continue
127 }
128 err := netlink.RouteDel(&route)
129 if !os.IsNotExist(err) && err != nil {
130 return fmt.Errorf("failed to delete DHCP route: %w", err)
131 }
132 }
133
134 if newRouter != nil {
135 err := netlink.RouteReplace(&netlink.Route{
136 Protocol: unix.RTPROT_DHCP,
137 Dst: &ipv4DefaultRoute,
138 Gw: newRouter,
139 Src: new.AssignedIP,
140 LinkIndex: iface.Attrs().Index,
141 Scope: netlink.SCOPE_UNIVERSE,
142 })
143 if err != nil {
144 return fmt.Errorf("failed to add default route via %s: %w", newRouter, err)
145 }
146 }
147 return nil
148 }
149}