blob: 354a3e8826d0ebb9020e4e4f0c4bd4f2442bf0f7 [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dhcp4c
import (
"encoding/binary"
"net"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
)
// Lease represents a DHCPv4 lease. It only consists of an IP, an expiration
// timestamp and options as all other relevant parts of the message have been
// normalized into their respective options. It also contains some smart
// getters for commonly-used options which extract only valid information from
// options.
type Lease struct {
AssignedIP net.IP
ExpiresAt time.Time
Options dhcpv4.Options
}
// SubnetMask returns the SubnetMask option or the default mask if not set or
// invalid.
// It returns nil if the lease is nil.
func (l *Lease) SubnetMask() net.IPMask {
if l == nil {
return nil
}
mask := net.IPMask(dhcpv4.GetIP(dhcpv4.OptionSubnetMask, l.Options))
// If given mask is not valid, use the default mask.
if _, bits := mask.Size(); bits != 32 {
mask = l.AssignedIP.DefaultMask()
}
return mask
}
// IPNet returns an IPNet from the assigned IP and subnet mask.
// It returns nil if the lease is nil.
func (l *Lease) IPNet() *net.IPNet {
if l == nil {
return nil
}
return &net.IPNet{
IP: l.AssignedIP,
Mask: l.SubnetMask(),
}
}
// Routes returns all routes assigned by a DHCP answer. It combines and
// normalizes data from the Router, StaticRoutingTable and ClasslessStaticRoute
// options.
func (l *Lease) Routes() []*dhcpv4.Route {
if l == nil {
return nil
}
// Note that this is different from l.IPNet() because we care about the
// network base address of the network instead of the assigned IP.
assignedNet := &net.IPNet{IP: l.AssignedIP.Mask(l.SubnetMask()), Mask: l.SubnetMask()}
// RFC 3442 Section DHCP Client Behavior:
// If the DHCP server returns both a Classless Static Routes option and
// a Router option, the DHCP client MUST ignore the Router option.
// Similarly, if the DHCP server returns both a Classless Static Routes
// option and a Static Routes option, the DHCP client MUST ignore the
// Static Routes option.
var routes dhcpv4.Routes
rawCIDRRoutes := l.Options.Get(dhcpv4.OptionClasslessStaticRoute)
if rawCIDRRoutes != nil {
// TODO(#96): This needs a logging story
// Ignore errors intentionally and just return what has been parsed
_ = routes.FromBytes(rawCIDRRoutes)
return sanitizeRoutes(routes, assignedNet)
}
// The Static Routes option contains legacy classful routes (i.e. routes
// whose mask is determined by the IP of the network).
// Each static route is expressed as a pair of IPs, the first one being
// the destination network and the second one being the router IP.
// See RFC 2132 Section 5.8 for further details.
legacyRouteIPs := dhcpv4.GetIPs(dhcpv4.OptionStaticRoutingTable, l.Options)
// Routes are only valid in pairs, cut the last one off if necessary
if len(legacyRouteIPs)%2 != 0 {
legacyRouteIPs = legacyRouteIPs[:len(legacyRouteIPs)-1]
}
for i := 0; i < len(legacyRouteIPs)/2; i++ {
dest := legacyRouteIPs[i*2]
if dest.IsUnspecified() {
// RFC 2132 Section 5.8:
// The default route (0.0.0.0) is an illegal destination for a
// static route.
continue
}
via := legacyRouteIPs[i*2+1]
destNet := net.IPNet{
// Apply the default mask just to make sure this is a valid route
IP: dest.Mask(dest.DefaultMask()),
Mask: dest.DefaultMask(),
}
routes = append(routes, &dhcpv4.Route{Dest: &destNet, Router: via})
}
for _, r := range dhcpv4.GetIPs(dhcpv4.OptionRouter, l.Options) {
if r.IsGlobalUnicast() || r.IsLinkLocalUnicast() {
routes = append(routes, &dhcpv4.Route{
Dest: &net.IPNet{IP: net.IPv4zero, Mask: net.IPv4Mask(0, 0, 0, 0)},
Router: r,
})
// Only one default router can exist, exit after the first one
break
}
}
return sanitizeRoutes(routes, assignedNet)
}
// sanitizeRoutes filters the list of routes by removing routes that are
// obviously invalid. It filters out routes according to the following criteria:
// 1. The route is not an interface route and its router is not a unicast or
// link-local address.
// 2. Each route's router must be reachable according to the routes listed
// before it and the assigned network.
// 3. The network mask must consist of all-ones followed by all-zeros. Non-
// contiguous routes are not allowed.
// 4. If multiple routes match the same destination, only the first one is kept.
// 5. Routes covering the loopback IP space (127.0.0.0/8) will be ignored if
// they are smaller than a /9 to prevent them from interfering with loopback
// IPs.
func sanitizeRoutes(routes []*dhcpv4.Route, assignedNet *net.IPNet) []*dhcpv4.Route {
var saneRoutes []*dhcpv4.Route
for _, route := range routes {
if route.Router != nil && !route.Router.IsUnspecified() {
if !(route.Router.IsGlobalUnicast() || route.Router.IsLinkLocalUnicast()) {
// Ignore non-unicast routers
continue
}
reachable := false
for _, r := range saneRoutes {
if r.Dest.Contains(route.Router) {
reachable = true
break
}
}
if assignedNet.Contains(route.Router) {
reachable = true
}
if !reachable {
continue
}
}
ones, bits := route.Dest.Mask.Size()
if bits == 0 && len(route.Dest.Mask) > 0 {
// Bitmask is not ones followed by zeros, i.e. invalid
continue
}
// Ignore routes that would be able to redirect loopback IPs
if route.Dest.IP.IsLoopback() && ones >= 8 {
continue
}
// Ignore routes that would shadow the implicit interface route
assignedOnes, _ := assignedNet.Mask.Size()
if assignedNet.IP.Equal(route.Dest.IP) && assignedOnes == ones {
continue
}
validDest := true
for _, r := range saneRoutes {
rOnes, _ := r.Dest.Mask.Size()
if r.Dest.IP.Equal(route.Dest.IP) && ones == rOnes {
// Exact duplicate, ignore
validDest = false
break
}
}
if validDest {
saneRoutes = append(saneRoutes, route)
}
}
return saneRoutes
}
// DNSServers represents an ordered collection of DNS servers
type DNSServers []net.IP
func (a DNSServers) Equal(b DNSServers) bool {
if len(a) == len(b) {
if len(a) == 0 {
return true // both are empty or nil
}
for i, aVal := range a {
if !aVal.Equal(b[i]) {
return false
}
}
return true
}
return false
}
func ip4toInt(ip net.IP) uint32 {
ip4 := ip.To4()
if ip4 == nil {
return 0
}
return binary.BigEndian.Uint32(ip4)
}
// DNSServers returns all unique valid DNS servers from the DHCP
// DomainNameServers options.
// It returns nil if the lease is nil.
func (l *Lease) DNSServers() DNSServers {
if l == nil {
return nil
}
rawServers := dhcpv4.GetIPs(dhcpv4.OptionDomainNameServer, l.Options)
var servers DNSServers
serversSeenMap := make(map[uint32]bool)
for _, s := range rawServers {
ip4Num := ip4toInt(s)
if s.IsGlobalUnicast() || s.IsLinkLocalUnicast() {
if serversSeenMap[ip4Num] {
continue
}
serversSeenMap[ip4Num] = true
servers = append(servers, s)
}
}
return servers
}