blob: dc3088b506a27361b7199f6345e890265ad71568 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun9601f262020-12-09 19:44:41 +01002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun9601f262020-12-09 19:44:41 +01003
Serge Bazanski216fe7b2021-05-21 18:36:16 +02004// Package callback contains minimal callbacks for configuring the kernel with
5// options received over DHCP.
Lorenz Brun9601f262020-12-09 19:44:41 +01006//
Serge Bazanski216fe7b2021-05-21 18:36:16 +02007// These directly configure the relevant kernel subsytems and need to own
8// certain parts of them as documented on a per- callback basis to make sure
9// that they can recover from restarts and crashes of the DHCP client.
10// The callbacks in here are not suitable for use in advanced network scenarios
11// like running multiple DHCP clients per interface via ClientIdentifier or
12// when running an external FIB manager. In these cases it's advised to extract
13// the necessary information from the lease in your own callback and
14// communicate it directly to the responsible entity.
Lorenz Brun9601f262020-12-09 19:44:41 +010015package callback
16
17import (
18 "fmt"
19 "math"
20 "net"
21 "os"
22 "time"
23
Lorenz Brun9601f262020-12-09 19:44:41 +010024 "github.com/insomniacslk/dhcp/dhcpv4"
25 "github.com/vishvananda/netlink"
26 "golang.org/x/sys/unix"
Serge Bazanski96043bc2021-10-05 12:10:13 +020027
Jan Schär07a39e22025-09-04 11:16:59 +020028 "source.monogon.dev/osbase/net/dhcp4c"
Lorenz Brun9601f262020-12-09 19:44:41 +010029)
30
31// Compose can be used to chain multiple callbacks
32func Compose(callbacks ...dhcp4c.LeaseCallback) dhcp4c.LeaseCallback {
Jan Schärba404a62024-07-11 10:46:27 +020033 return func(lease *dhcp4c.Lease) error {
Lorenz Brun9601f262020-12-09 19:44:41 +010034 for _, cb := range callbacks {
Jan Schärba404a62024-07-11 10:46:27 +020035 if err := cb(lease); err != nil {
Lorenz Brun9601f262020-12-09 19:44:41 +010036 return err
37 }
38 }
39 return nil
40 }
41}
42
43func isIPNetEqual(a, b *net.IPNet) bool {
44 if a == b {
45 return true
46 }
47 if a == nil || b == nil {
48 return false
49 }
50 aOnes, aBits := a.Mask.Size()
51 bOnes, bBits := b.Mask.Size()
52 return a.IP.Equal(b.IP) && aOnes == bOnes && aBits == bBits
53}
54
Serge Bazanski216fe7b2021-05-21 18:36:16 +020055// ManageIP sets up and tears down the assigned IP address. It takes exclusive
56// ownership of all IPv4 addresses on the given interface which do not have
57// IFA_F_PERMANENT set, so it's not possible to run multiple dynamic addressing
Lorenz Brun9601f262020-12-09 19:44:41 +010058// clients on a single interface.
59func ManageIP(iface netlink.Link) dhcp4c.LeaseCallback {
Jan Schärba404a62024-07-11 10:46:27 +020060 return func(lease *dhcp4c.Lease) error {
61 newNet := lease.IPNet()
Lorenz Brun9601f262020-12-09 19:44:41 +010062
63 addrs, err := netlink.AddrList(iface, netlink.FAMILY_V4)
64 if err != nil {
65 return fmt.Errorf("netlink failed to list addresses: %w", err)
66 }
67
68 for _, addr := range addrs {
69 if addr.Flags&unix.IFA_F_PERMANENT == 0 {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020070 // Linux identifies addreses by IP, mask and peer (see
71 // net/ipv4/devinet.find_matching_ifa in Linux 5.10).
72 // So don't touch addresses which match on these properties as
73 // AddrReplace will atomically reconfigure them anyways without
74 // interrupting things.
Jan Schärba404a62024-07-11 10:46:27 +020075 if isIPNetEqual(addr.IPNet, newNet) && addr.Peer == nil && lease != nil {
Lorenz Brun9601f262020-12-09 19:44:41 +010076 continue
77 }
78
79 if err := netlink.AddrDel(iface, &addr); !os.IsNotExist(err) && err != nil {
80 return fmt.Errorf("failed to delete address: %w", err)
81 }
82 }
83 }
84
Jan Schärba404a62024-07-11 10:46:27 +020085 if lease != nil {
86 remainingLifetimeSecs := int(math.Ceil(time.Until(lease.ExpiresAt).Seconds()))
87 newBroadcastIP := dhcpv4.GetIP(dhcpv4.OptionBroadcastAddress, lease.Options)
Lorenz Brun9601f262020-12-09 19:44:41 +010088 if err := netlink.AddrReplace(iface, &netlink.Addr{
89 IPNet: newNet,
90 ValidLft: remainingLifetimeSecs,
91 PreferedLft: remainingLifetimeSecs,
92 Broadcast: newBroadcastIP,
93 }); err != nil {
94 return fmt.Errorf("failed to update address: %w", err)
95 }
96 }
97 return nil
98 }
99}
100
Lorenz Brunfdb73222021-12-13 05:19:25 +0100101// ManageRoutes installs and removes routes according to the current DHCP lease,
102// including the default route (if any).
103// It takes ownership of all RTPROTO_DHCP routes on the given interface, so it's
104// not possible to run multiple DHCP clients on the given interface.
105func ManageRoutes(iface netlink.Link) dhcp4c.LeaseCallback {
Jan Schärba404a62024-07-11 10:46:27 +0200106 return func(lease *dhcp4c.Lease) error {
107 newRoutes := lease.Routes()
Lorenz Brun9601f262020-12-09 19:44:41 +0100108
109 dhcpRoutes, err := netlink.RouteListFiltered(netlink.FAMILY_V4, &netlink.Route{
110 Protocol: unix.RTPROT_DHCP,
111 LinkIndex: iface.Attrs().Index,
112 }, netlink.RT_FILTER_OIF|netlink.RT_FILTER_PROTOCOL)
113 if err != nil {
114 return fmt.Errorf("netlink failed to list routes: %w", err)
115 }
Lorenz Brun9601f262020-12-09 19:44:41 +0100116 for _, route := range dhcpRoutes {
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200117 // Don't remove routes which can be atomically replaced by
118 // RouteReplace to prevent potential traffic disruptions.
Lorenz Brunfdb73222021-12-13 05:19:25 +0100119 //
120 // This is O(n^2) but the number of routes is bounded by the size
121 // of a DHCP packet (around 100 routes). Sorting both would be
122 // be marginally faster for large amounts of routes only and in 99%
123 // of cases it's going to be <5 routes.
124 var found bool
125 for _, newRoute := range newRoutes {
126 if isIPNetEqual(newRoute.Dest, route.Dst) {
127 found = true
128 break
129 }
Lorenz Brun9601f262020-12-09 19:44:41 +0100130 }
Lorenz Brunfdb73222021-12-13 05:19:25 +0100131 if !found {
132 err := netlink.RouteDel(&route)
133 if !os.IsNotExist(err) && err != nil {
134 return fmt.Errorf("failed to delete DHCP route: %w", err)
135 }
Lorenz Brun9601f262020-12-09 19:44:41 +0100136 }
137 }
138
Lorenz Brunfdb73222021-12-13 05:19:25 +0100139 for _, route := range newRoutes {
140 newRoute := netlink.Route{
Lorenz Brun9601f262020-12-09 19:44:41 +0100141 Protocol: unix.RTPROT_DHCP,
Lorenz Brunfdb73222021-12-13 05:19:25 +0100142 Dst: route.Dest,
143 Gw: route.Router,
Jan Schärba404a62024-07-11 10:46:27 +0200144 Src: lease.AssignedIP,
Lorenz Brun9601f262020-12-09 19:44:41 +0100145 LinkIndex: iface.Attrs().Index,
146 Scope: netlink.SCOPE_UNIVERSE,
Lorenz Brunfdb73222021-12-13 05:19:25 +0100147 }
148 // Routes with a non-L3 gateway are link-scoped
149 if route.Router.IsUnspecified() {
150 newRoute.Scope = netlink.SCOPE_LINK
151 }
152 err := netlink.RouteReplace(&newRoute)
Lorenz Brun9601f262020-12-09 19:44:41 +0100153 if err != nil {
Lorenz Brunfdb73222021-12-13 05:19:25 +0100154 return fmt.Errorf("failed to add %s: %w", route, err)
Lorenz Brun9601f262020-12-09 19:44:41 +0100155 }
156 }
157 return nil
158 }
159}