blob: fefe09225ccea18fac0a0814e9b7982e1f4fccb8 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Lorenz Brun56a7ae62020-10-29 11:03:30 +01002// SPDX-License-Identifier: Apache-2.0
Lorenz Brun56a7ae62020-10-29 11:03:30 +01003
4package dhcp4c
5
6import (
Lorenz Brundbac6cc2020-11-30 10:57:26 +01007 "encoding/binary"
Lorenz Brun56a7ae62020-10-29 11:03:30 +01008 "net"
9 "time"
10
11 "github.com/insomniacslk/dhcp/dhcpv4"
12)
13
Serge Bazanski216fe7b2021-05-21 18:36:16 +020014// Lease represents a DHCPv4 lease. It only consists of an IP, an expiration
15// timestamp and options as all other relevant parts of the message have been
16// normalized into their respective options. It also contains some smart
17// getters for commonly-used options which extract only valid information from
18// options.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010019type Lease struct {
20 AssignedIP net.IP
21 ExpiresAt time.Time
22 Options dhcpv4.Options
23}
24
Serge Bazanski216fe7b2021-05-21 18:36:16 +020025// SubnetMask returns the SubnetMask option or the default mask if not set or
26// invalid.
Lorenz Brun56a7ae62020-10-29 11:03:30 +010027// It returns nil if the lease is nil.
28func (l *Lease) SubnetMask() net.IPMask {
29 if l == nil {
30 return nil
31 }
32 mask := net.IPMask(dhcpv4.GetIP(dhcpv4.OptionSubnetMask, l.Options))
Serge Bazanski216fe7b2021-05-21 18:36:16 +020033 // If given mask is not valid, use the default mask.
34 if _, bits := mask.Size(); bits != 32 {
Lorenz Brun56a7ae62020-10-29 11:03:30 +010035 mask = l.AssignedIP.DefaultMask()
36 }
37 return mask
38}
39
40// IPNet returns an IPNet from the assigned IP and subnet mask.
41// It returns nil if the lease is nil.
42func (l *Lease) IPNet() *net.IPNet {
43 if l == nil {
44 return nil
45 }
46 return &net.IPNet{
47 IP: l.AssignedIP,
48 Mask: l.SubnetMask(),
49 }
50}
51
Lorenz Brunfdb73222021-12-13 05:19:25 +010052// Routes returns all routes assigned by a DHCP answer. It combines and
53// normalizes data from the Router, StaticRoutingTable and ClasslessStaticRoute
54// options.
55func (l *Lease) Routes() []*dhcpv4.Route {
Lorenz Brun56a7ae62020-10-29 11:03:30 +010056 if l == nil {
57 return nil
58 }
Lorenz Brunfdb73222021-12-13 05:19:25 +010059
60 // Note that this is different from l.IPNet() because we care about the
61 // network base address of the network instead of the assigned IP.
62 assignedNet := &net.IPNet{IP: l.AssignedIP.Mask(l.SubnetMask()), Mask: l.SubnetMask()}
63
64 // RFC 3442 Section DHCP Client Behavior:
65 // If the DHCP server returns both a Classless Static Routes option and
66 // a Router option, the DHCP client MUST ignore the Router option.
67 // Similarly, if the DHCP server returns both a Classless Static Routes
68 // option and a Static Routes option, the DHCP client MUST ignore the
69 // Static Routes option.
70 var routes dhcpv4.Routes
71 rawCIDRRoutes := l.Options.Get(dhcpv4.OptionClasslessStaticRoute)
72 if rawCIDRRoutes != nil {
73 // TODO(#96): This needs a logging story
74 // Ignore errors intentionally and just return what has been parsed
75 _ = routes.FromBytes(rawCIDRRoutes)
76 return sanitizeRoutes(routes, assignedNet)
77 }
78 // The Static Routes option contains legacy classful routes (i.e. routes
79 // whose mask is determined by the IP of the network).
80 // Each static route is expressed as a pair of IPs, the first one being
81 // the destination network and the second one being the router IP.
82 // See RFC 2132 Section 5.8 for further details.
83 legacyRouteIPs := dhcpv4.GetIPs(dhcpv4.OptionStaticRoutingTable, l.Options)
84 // Routes are only valid in pairs, cut the last one off if necessary
85 if len(legacyRouteIPs)%2 != 0 {
86 legacyRouteIPs = legacyRouteIPs[:len(legacyRouteIPs)-1]
87 }
88 for i := 0; i < len(legacyRouteIPs)/2; i++ {
89 dest := legacyRouteIPs[i*2]
90 if dest.IsUnspecified() {
91 // RFC 2132 Section 5.8:
92 // The default route (0.0.0.0) is an illegal destination for a
93 // static route.
94 continue
95 }
96 via := legacyRouteIPs[i*2+1]
97 destNet := net.IPNet{
98 // Apply the default mask just to make sure this is a valid route
99 IP: dest.Mask(dest.DefaultMask()),
100 Mask: dest.DefaultMask(),
101 }
102 routes = append(routes, &dhcpv4.Route{Dest: &destNet, Router: via})
103 }
104 for _, r := range dhcpv4.GetIPs(dhcpv4.OptionRouter, l.Options) {
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100105 if r.IsGlobalUnicast() || r.IsLinkLocalUnicast() {
Lorenz Brunfdb73222021-12-13 05:19:25 +0100106 routes = append(routes, &dhcpv4.Route{
107 Dest: &net.IPNet{IP: net.IPv4zero, Mask: net.IPv4Mask(0, 0, 0, 0)},
108 Router: r,
109 })
110 // Only one default router can exist, exit after the first one
111 break
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100112 }
113 }
Lorenz Brunfdb73222021-12-13 05:19:25 +0100114 return sanitizeRoutes(routes, assignedNet)
115}
116
117// sanitizeRoutes filters the list of routes by removing routes that are
118// obviously invalid. It filters out routes according to the following criteria:
Tim Windelschmidt99e15112025-02-05 17:38:16 +0100119// 1. The route is not an interface route and its router is not a unicast or
120// link-local address.
121// 2. Each route's router must be reachable according to the routes listed
122// before it and the assigned network.
123// 3. The network mask must consist of all-ones followed by all-zeros. Non-
124// contiguous routes are not allowed.
125// 4. If multiple routes match the same destination, only the first one is kept.
126// 5. Routes covering the loopback IP space (127.0.0.0/8) will be ignored if
127// they are smaller than a /9 to prevent them from interfering with loopback
128// IPs.
Lorenz Brunfdb73222021-12-13 05:19:25 +0100129func sanitizeRoutes(routes []*dhcpv4.Route, assignedNet *net.IPNet) []*dhcpv4.Route {
130 var saneRoutes []*dhcpv4.Route
131 for _, route := range routes {
132 if route.Router != nil && !route.Router.IsUnspecified() {
Tim Windelschmidt683b62b2024-04-18 23:40:33 +0200133 if !route.Router.IsGlobalUnicast() && !route.Router.IsLinkLocalUnicast() {
Lorenz Brunfdb73222021-12-13 05:19:25 +0100134 // Ignore non-unicast routers
135 continue
136 }
137 reachable := false
138 for _, r := range saneRoutes {
139 if r.Dest.Contains(route.Router) {
140 reachable = true
141 break
142 }
143 }
144 if assignedNet.Contains(route.Router) {
145 reachable = true
146 }
147 if !reachable {
148 continue
149 }
150 }
151 ones, bits := route.Dest.Mask.Size()
152 if bits == 0 && len(route.Dest.Mask) > 0 {
153 // Bitmask is not ones followed by zeros, i.e. invalid
154 continue
155 }
156 // Ignore routes that would be able to redirect loopback IPs
157 if route.Dest.IP.IsLoopback() && ones >= 8 {
158 continue
159 }
160 // Ignore routes that would shadow the implicit interface route
161 assignedOnes, _ := assignedNet.Mask.Size()
162 if assignedNet.IP.Equal(route.Dest.IP) && assignedOnes == ones {
163 continue
164 }
165 validDest := true
166 for _, r := range saneRoutes {
167 rOnes, _ := r.Dest.Mask.Size()
168 if r.Dest.IP.Equal(route.Dest.IP) && ones == rOnes {
169 // Exact duplicate, ignore
170 validDest = false
171 break
172 }
173 }
174 if validDest {
175 saneRoutes = append(saneRoutes, route)
176 }
177 }
178 return saneRoutes
Lorenz Brun56a7ae62020-10-29 11:03:30 +0100179}
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100180
181// DNSServers represents an ordered collection of DNS servers
182type DNSServers []net.IP
183
184func (a DNSServers) Equal(b DNSServers) bool {
185 if len(a) == len(b) {
186 if len(a) == 0 {
187 return true // both are empty or nil
188 }
189 for i, aVal := range a {
190 if !aVal.Equal(b[i]) {
191 return false
192 }
193 }
194 return true
195 }
196 return false
197
198}
199
200func ip4toInt(ip net.IP) uint32 {
201 ip4 := ip.To4()
202 if ip4 == nil {
203 return 0
204 }
205 return binary.BigEndian.Uint32(ip4)
206}
207
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200208// DNSServers returns all unique valid DNS servers from the DHCP
209// DomainNameServers options.
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100210// It returns nil if the lease is nil.
211func (l *Lease) DNSServers() DNSServers {
212 if l == nil {
213 return nil
214 }
215 rawServers := dhcpv4.GetIPs(dhcpv4.OptionDomainNameServer, l.Options)
216 var servers DNSServers
Lorenz Brun87bf0bf2022-01-13 14:27:36 +0100217 serversSeenMap := make(map[uint32]bool)
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100218 for _, s := range rawServers {
219 ip4Num := ip4toInt(s)
Lorenz Brun87bf0bf2022-01-13 14:27:36 +0100220 if s.IsGlobalUnicast() || s.IsLinkLocalUnicast() {
221 if serversSeenMap[ip4Num] {
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100222 continue
223 }
Lorenz Brun87bf0bf2022-01-13 14:27:36 +0100224 serversSeenMap[ip4Num] = true
Lorenz Brundbac6cc2020-11-30 10:57:26 +0100225 servers = append(servers, s)
226 }
227 }
228 return servers
229}