Files
troubleshoot/pkg/collect/host_subnetavailable.go

283 lines
9.7 KiB
Go

package collect
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/apparentlymart/go-cidr/cidr"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/klog/v2"
)
type SubnetStatus string
const (
SubnetStatusAvailable = "a-subnet-is-available"
SubnetStatusNoneAvailable = "no-subnet-available"
)
type SubnetAvailableResult struct {
CIDRRangeAlloc string `json:"CIDRRangeAlloc"`
DesiredCIDR int `json:"desiredCIDR"`
// If subnet-available, at least 1 of the DesiredCIDR size is available within CIDRRangeAlloc
Status SubnetStatus `json:"status"`
}
type CollectHostSubnetAvailable struct {
hostCollector *troubleshootv1beta2.SubnetAvailable
BundlePath string
}
func (c *CollectHostSubnetAvailable) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Subnet Available")
}
func (c *CollectHostSubnetAvailable) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostSubnetAvailable) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
procNetRoute, err := os.ReadFile("/proc/net/route")
if err != nil {
return nil, errors.Wrap(err, "failed to read contents of /proc/net/route")
}
routes, err := parseProcNetRoute(string(procNetRoute))
if err != nil {
return nil, errors.Wrap(err, "failed to parse /proc/net/route")
}
klog.V(3).Infof("Routes: %+v\n", routes)
// IPv4 only right now...
if c.hostCollector.DesiredCIDR < 1 || c.hostCollector.DesiredCIDR > 32 {
return nil, errors.Wrap(err, fmt.Sprintf("CIDR range size %d invalid, must be between 1 and 32", c.hostCollector.DesiredCIDR))
}
splitCIDRRangeAlloc := strings.Split(c.hostCollector.CIDRRangeAlloc, "/")
if len(splitCIDRRangeAlloc) != 2 {
return nil, errors.Wrap(err, fmt.Sprintf("CIDRRangeAlloc value %s invalid, expected format x.x.x.x/##", c.hostCollector.CIDRRangeAlloc))
}
maskInt, err := strconv.Atoi(splitCIDRRangeAlloc[1])
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("CIDRRangeAlloc mask %s invalid, expected integer", splitCIDRRangeAlloc[1]))
}
if maskInt < 0 || maskInt > 32 {
return nil, errors.Wrap(err, fmt.Sprintf("CIDRRangeAlloc mask %d invalid, must be between 0 and 32", maskInt))
}
cidrRangeAllocIPNet := net.IPNet{
IP: net.ParseIP(splitCIDRRangeAlloc[0]),
Mask: net.CIDRMask(maskInt, 32),
}
result := SubnetAvailableResult{}
result.CIDRRangeAlloc = c.hostCollector.CIDRRangeAlloc
result.DesiredCIDR = c.hostCollector.DesiredCIDR
available, err := isASubnetAvailableInCIDR(c.hostCollector.DesiredCIDR, &cidrRangeAllocIPNet, &routes)
if err != nil {
return nil, errors.Wrap(err, "failed to determine if desired CIDR is available within subnet")
}
if available == true {
result.Status = SubnetStatusAvailable
} else {
result.Status = SubnetStatusNoneAvailable
}
b, err := json.Marshal(result)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal result")
}
collectorName := c.hostCollector.CollectorName
if collectorName == "" {
collectorName = "result"
}
name := filepath.Join("host-collectors/subnetAvailable", collectorName+".json")
output := NewResult()
output.SaveResult(c.BundlePath, name, bytes.NewBuffer(b))
return map[string][]byte{
name: b,
}, nil
}
type systemRoutes []systemRoute
type systemRoute struct {
Iface string
DestNet net.IPNet
Gateway net.IP
Metric uint32
}
// Parses the output of /proc/net/route into something useful
// This only deals with IPv4 - another file /proc/net/ipv6_route deals with IPv6 (not implemented here)
func parseProcNetRoute(input string) (systemRoutes, error) {
routes := systemRoutes{}
for _, line := range strings.Split(input, "\n") {
if len(line) < 4 {
// Likely a blank line?
continue
}
if line[0:5] == "Iface" {
continue
}
splitLine := strings.Split(strings.TrimSpace(line), "\t")
if len(splitLine) != 11 {
return []systemRoute{}, errors.Errorf("invalid /proc/net/route line '%s', expected 11 columns got %d", line, len(splitLine))
}
dest, err := hex.DecodeString(strings.TrimSpace(splitLine[1]))
if err != nil {
return []systemRoute{}, errors.Wrapf(err, "cannot parse dest column (index 1) for /proc/net/route line '%s'", line)
}
destStr := fmt.Sprintf("%d.%d.%d.%d", dest[3], dest[2], dest[1], dest[0])
gw, err := hex.DecodeString(strings.TrimSpace(splitLine[2]))
if err != nil {
return []systemRoute{}, errors.Wrapf(err, "cannot parse gateway column (index 2) for /proc/net/route line '%s'", line)
}
gwStr := fmt.Sprintf("%d.%d.%d.%d", gw[3], gw[2], gw[1], gw[0])
mask, err := hex.DecodeString(strings.TrimSpace(splitLine[7]))
if err != nil {
return []systemRoute{}, errors.Wrapf(err, "cannot parse mask column (index 7) for /proc/net/route line '%s'", line)
}
maskStr := fmt.Sprintf("%d.%d.%d.%d", mask[3], mask[2], mask[1], mask[0])
maskBytes := []byte{}
for _, v := range strings.Split(maskStr, ".") {
maskByte, err := strconv.Atoi(v)
if err != nil {
return []systemRoute{}, errors.Wrapf(err, "cannot convert mask octet '%s' to byte", v)
}
maskBytes = append(maskBytes, byte(maskByte))
}
metric, err := strconv.Atoi(strings.TrimSpace(splitLine[6]))
if err != nil {
return []systemRoute{}, errors.Wrapf(err, "cannot parse metric column (index 6) for /proc/net/route line '%s'", line)
}
routes = append(routes, systemRoute{
Iface: strings.TrimSpace(splitLine[0]),
DestNet: net.IPNet{
IP: net.ParseIP(destStr),
Mask: net.IPv4Mask(maskBytes[0], maskBytes[1], maskBytes[2], maskBytes[3]),
},
Gateway: net.ParseIP(gwStr),
Metric: uint32(metric),
})
}
return routes, nil
}
// Credit: https://github.com/replicatedhq/kURL/blob/main/kurl_util/cmd/subnet/main.go findAvailableSubnet
// TODOLATER: consolidate some of this logic into a unified library? will need a bit of refactoring if so
//
// isASubnetAvailableInCIDR will check if a subnet of cidrRange size is available within subnetRange (IPv4 only), checking against system routes for conflicts
func isASubnetAvailableInCIDR(cidrRange int, subnetRange *net.IPNet, routes *systemRoutes) (bool, error) {
// Sanity check that the CIDR range size is IPv4 valid (/0 to /32)
if cidrRange < 1 || cidrRange > 32 {
return false, errors.New(fmt.Sprintf("CIDR range size %d invalid, must be between 1 and 32", cidrRange))
}
// Check that cidrRange is equal to or smaller than the subnet size of subnetRange
// Always exit false if not...
subnetRangeSize, subnetRangeSizeBits := subnetRange.Mask.Size()
if subnetRangeSizeBits != 32 {
return false, errors.New(fmt.Sprintf("subnetRange size is not IPv4 compatible? expected 32 got %d", subnetRangeSizeBits))
}
// NOTE: reversed operator as we're talking about CIDR blocks (smaller integer = more IPs)
if subnetRangeSize > cidrRange {
return false, errors.New(fmt.Sprintf("subnetRange size (%d) must be larger than or equal to cidrRange size (%d), can't check if a range larger than itself is available", subnetRangeSize, cidrRange))
}
// Find the start IP of subnetRange, this will become the first subnet to be tested (with cidrRange as the size)
startIP, _ := cidr.AddressRange(subnetRange)
_, subnet, err := net.ParseCIDR(fmt.Sprintf("%s/%d", startIP, cidrRange))
if err != nil {
return false, errors.Wrap(err, "parse cidr")
}
for {
// This is the extents of the subnet to be tested
firstIP, lastIP := cidr.AddressRange(subnet)
klog.V(2).Infof("Checking subnet: %s firstIP: %s lastIP: %s\n", subnet.String(), firstIP, lastIP)
// Make sure the smaller subnet is (still) within the large subnetRange. If subnetRange has been exhausted one of these will be false
if !subnetRange.Contains(firstIP) || !subnetRange.Contains(lastIP) {
return false, nil
}
// Check if any system routes overlap with the smaller subnet being tested
route := findFirstOverlappingRoute(subnet, routes)
if route == nil {
// No system routes match, this (smaller) subnet is available
klog.V(1).Infof("Subnet %s is available", subnet.String())
return true, nil
}
// Try the next subnet in the range
subnet, _ = cidr.NextSubnet(subnet, cidrRange)
}
}
// findFirstOverlappingRoute will return the first overlapping route with the subnet specified
func findFirstOverlappingRoute(subnet *net.IPNet, routes *systemRoutes) *systemRoute {
// NOTE: IPv4 specific
defaultRoute := net.IPNet{
IP: net.IPv4(0, 0, 0, 0),
Mask: net.CIDRMask(0, 32),
}
for _, route := range *routes {
// Exclude default routes (0.0.0.0/0)
if route.DestNet.IP.Equal(defaultRoute.IP) && route.DestNet.Mask.String() == defaultRoute.Mask.String() {
continue
}
klog.V(2).Infof("Checking if route %s overlaps with subnet %s - ", &route.DestNet, subnet)
// TODOLATER: can we use cidr.VerifyNoOverlap to replace this? tests fail right now trying to do so...
//if cidr.VerifyNoOverlap([]*net.IPNet{subnet}, &route.DestNet) != nil {
if netOverlaps(&route.DestNet, subnet) {
klog.V(2).Infof("Overlaps\n")
return &route
} else {
klog.V(2).Infof("No overlap\n")
}
}
klog.V(2).Infof("Subnet %s has no overlap with any system routes\n", subnet)
return nil
}
func netOverlaps(n1, n2 *net.IPNet) bool {
n1FirstIP, n1LastIP := cidr.AddressRange(n1)
n2FirstIP, n2LastIP := cidr.AddressRange(n2)
// Check if the first net contains either the first or last IP of the second net
if n1.Contains(n2FirstIP) || n1.Contains(n2LastIP) {
return true
}
// Now do the reverse: check if the second net contains either the first or last IP of the first net
if n2.Contains(n1FirstIP) || n2.Contains(n1LastIP) {
return true
}
return false
}
func (c *CollectHostSubnetAvailable) RemoteCollect(progressChan chan<- interface{}) (map[string][]byte, error) {
return nil, ErrRemoteCollectorNotImplemented
}