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