internal/node/hybrid/ip_validator.go (178 lines of code) (raw):

package hybrid import ( "fmt" "net" "github.com/aws/aws-sdk-go-v2/service/eks/types" apimachinerynet "k8s.io/apimachinery/pkg/util/net" ) const ( nodeIPFlag = "node-ip" hostnameOverrideFlag = "hostname-override" ) // Network interfaces with the host's network stack. type Network interface { LookupIP(host string) ([]net.IP, error) ResolveBindAddress(bindAddress net.IP) (net.IP, error) InterfaceAddrs() ([]net.Addr, error) } // defaultKubeletNetwork provides the network util functions used by kubelet. type defaultKubeletNetwork struct{} func (u defaultKubeletNetwork) LookupIP(host string) ([]net.IP, error) { return net.LookupIP(host) } func (u defaultKubeletNetwork) ResolveBindAddress(bindAddress net.IP) (net.IP, error) { return apimachinerynet.ResolveBindAddress(bindAddress) } func (u defaultKubeletNetwork) InterfaceAddrs() ([]net.Addr, error) { return net.InterfaceAddrs() } func containsIP(cidr string, ip net.IP) (bool, error) { _, ipnet, err := net.ParseCIDR(cidr) if err != nil { return false, err } return ipnet.Contains(ip), nil } func isIPInCIDRs(ip net.IP, cidrs []string) (bool, error) { if ip.To4() == nil { return false, fmt.Errorf("error: ip is invalid") } for _, cidr := range cidrs { if inNetwork, err := containsIP(cidr, ip); err != nil { return false, fmt.Errorf("error checking IP in CIDR %s: %w", cidr, err) } else if inNetwork { return true, nil } } return false, nil } func extractCIDRsFromNodeNetworks(networks []types.RemoteNodeNetwork) []string { var cidrs []string for _, network := range networks { for _, cidr := range network.Cidrs { if cidr != "" { cidrs = append(cidrs, cidr) } } } return cidrs } func extractNodeIPFromFlags(kubeletArgs []string) (net.IP, error) { ipStr := extractFlagValue(kubeletArgs, nodeIPFlag) if ipStr != "" { ip := net.ParseIP(ipStr) if ip == nil { return nil, fmt.Errorf("invalid ip %s in --node-ip flag. only 1 IPv4 address is allowed", ipStr) } else if ip.To4() == nil { return nil, fmt.Errorf("invalid IPv6 address %s in --node-ip flag. only IPv4 is supported", ipStr) } return ip, nil } //--node-ip flag not set return nil, nil } func validateClusterRemoteNetworkConfig(cluster *types.Cluster) error { if cluster.RemoteNetworkConfig == nil { return fmt.Errorf("remote network config is not set for cluster %s", *cluster.Name) } if cluster.RemoteNetworkConfig.RemoteNodeNetworks == nil { return fmt.Errorf("remote node networks not found in remote network config for cluster %s", *cluster.Name) } return nil } // Validate given node IP belongs to the current host. // // validateNodeIP adapts the unexported 'validateNodeIP' function from kubelet. // Source: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_node_status.go#L796 func validateNodeIP(nodeIP net.IP, interfaceAddrs func() ([]net.Addr, error)) error { // Honor IP limitations set in setNodeStatus() if nodeIP.To4() == nil && nodeIP.To16() == nil { return fmt.Errorf("nodeIP must be a valid IP address") } if nodeIP.IsLoopback() { return fmt.Errorf("nodeIP can't be loopback address") } if nodeIP.IsMulticast() { return fmt.Errorf("nodeIP can't be a multicast address") } if nodeIP.IsLinkLocalUnicast() { return fmt.Errorf("nodeIP can't be a link-local unicast address") } if nodeIP.IsUnspecified() { return fmt.Errorf("nodeIP can't be an all zeros address") } addrs, err := interfaceAddrs() if err != nil { return err } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip != nil && ip.Equal(nodeIP) { return nil } } return fmt.Errorf("node IP: %q not found in the host's network interfaces", nodeIP.String()) } // getNodeIP determines the node's IP address based on kubelet configuration and system information. func getNodeIP(kubeletArgs []string, nodeName string, network Network) (net.IP, error) { // Follows algorithm used by kubelet to assign nodeIP // Implementation adapted for hybrid nodes // 1) Use nodeIP if set (and not "0.0.0.0"/"::") // 2) If the user has specified an IP to HostnameOverride, use it (not allowed for hybrid nodes) // 3) Lookup the IP from node name by DNS // 4) Try to get the IP from the network interface used as default gateway // Source: https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/nodestatus/setters.go#L206 nodeIP, err := extractNodeIPFromFlags(kubeletArgs) if err != nil { return nil, err } var ipAddr net.IP nodeIPSpecified := nodeIP != nil && nodeIP.To4() != nil && !nodeIP.IsUnspecified() if nodeIPSpecified { ipAddr = nodeIP } else { // If using SSM, the node name will be set at initialization to the SSM instance ID, // so it won't resolve to anything via DNS, hence we're only checking in the case of IAM-RA if nodeName != "" { addrs, _ := network.LookupIP(nodeName) for _, addr := range addrs { if err = validateNodeIP(addr, network.InterfaceAddrs); addr.To4() != nil && err == nil { ipAddr = addr break } } } if ipAddr == nil { ipAddr, err = network.ResolveBindAddress(nodeIP) } if err != nil || ipAddr == nil { // We tried everything we could, but the IP address wasn't fetchable; error out return nil, fmt.Errorf("couldn't get ip address of node: %w", err) } } return ipAddr, nil } func validateIPInRemoteNodeNetwork(ipAddr net.IP, remoteNodeNetwork []types.RemoteNodeNetwork) error { nodeNetworkCidrs := extractCIDRsFromNodeNetworks(remoteNodeNetwork) if validIP, err := isIPInCIDRs(ipAddr, nodeNetworkCidrs); err != nil { return err } else if !validIP { // TODO: Update url with specific node IP troubleshooting section return fmt.Errorf( "node IP %s is not in any of the remote network CIDR blocks: %s. "+ "See https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-troubleshooting.html or use --skip node-ip-validation", ipAddr, nodeNetworkCidrs) } return nil } func (hnp *HybridNodeProvider) ValidateNodeIP() error { if hnp.cluster == nil { hnp.Logger().Info("Node IP validation skipped") return nil } else { hnp.logger.Info("Validating Node IP...") // Only check flags set by user in the config file to help determine IP: // - node-ip and hostname-override are only available as flags and cannot be set via spec.kubelet.config // - Hybrid nodes does not set --node-ip // - Hybrid nodes sets --hostname-override to either the IAM-RA Node name or the SSM instance ID, which is checked separately for DNS kubeletArgs := hnp.nodeConfig.Spec.Kubelet.Flags var iamNodeName string if hnp.nodeConfig.IsIAMRolesAnywhere() { iamNodeName = hnp.nodeConfig.Status.Hybrid.NodeName } nodeIp, err := getNodeIP(kubeletArgs, iamNodeName, hnp.network) if err != nil { return err } cluster := hnp.cluster if validateClusterRemoteNetworkConfig(cluster) != nil { return err } if err = validateIPInRemoteNodeNetwork(nodeIp, cluster.RemoteNetworkConfig.RemoteNodeNetworks); err != nil { return err } } return nil }