diff --git a/cmd/kubeadm/app/util/config/common.go b/cmd/kubeadm/app/util/config/common.go index 329397127e5..7a0650ea37c 100644 --- a/cmd/kubeadm/app/util/config/common.go +++ b/cmd/kubeadm/app/util/config/common.go @@ -123,6 +123,14 @@ func VerifyAPIServerBindAddress(address string) error { if ip == nil { return errors.Errorf("cannot parse IP address: %s", address) } + // There are users with network setups where default routes are present, but network interfaces + // use only link-local addresses (e.g. as described in RFC5549). + // In many cases that matching global unicast IP address can be found on loopback interface, + // so kubeadm allows users to specify address=Loopback for handling supporting the scenario above. + // Nb. SetAPIEndpointDynamicDefaults will try to translate loopback to a valid address afterwards + if ip.IsLoopback() { + return nil + } if !ip.IsGlobalUnicast() { return errors.Errorf("cannot use %q as the bind address for the API Server", address) } diff --git a/cmd/kubeadm/app/util/config/common_test.go b/cmd/kubeadm/app/util/config/common_test.go index a9f1d6f0e2a..c0d7c0893fe 100644 --- a/cmd/kubeadm/app/util/config/common_test.go +++ b/cmd/kubeadm/app/util/config/common_test.go @@ -161,13 +161,13 @@ func TestVerifyAPIServerBindAddress(t *testing.T) { address: "2001:db8:85a3::8a2e:370:7334", }, { - name: "invalid address: not a global unicast 0.0.0.0", - address: "0.0.0.0", - expectedError: true, + name: "valid address 127.0.0.1", + address: "127.0.0.1", + expectedError: false, }, { - name: "invalid address: not a global unicast 127.0.0.1", - address: "127.0.0.1", + name: "invalid address: not a global unicast 0.0.0.0", + address: "0.0.0.0", expectedError: true, }, { diff --git a/cmd/kubeadm/app/util/config/initconfiguration.go b/cmd/kubeadm/app/util/config/initconfiguration.go index b852cecf3d5..56d9d782593 100644 --- a/cmd/kubeadm/app/util/config/initconfiguration.go +++ b/cmd/kubeadm/app/util/config/initconfiguration.go @@ -26,9 +26,10 @@ import ( "github.com/pkg/errors" "k8s.io/klog" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + netutil "k8s.io/apimachinery/pkg/util/net" bootstraputil "k8s.io/cluster-bootstrap/token/util" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" @@ -112,6 +113,22 @@ func SetAPIEndpointDynamicDefaults(cfg *kubeadmapi.APIEndpoint) error { if addressIP == nil && cfg.AdvertiseAddress != "" { return errors.Errorf("couldn't use \"%s\" as \"apiserver-advertise-address\", must be ipv4 or ipv6 address", cfg.AdvertiseAddress) } + + // kubeadm allows users to specify address=Loopback as a selector for global unicast IP address that can be found on loopback interface. + // e.g. This is required for network setups where default routes are present, but network interfaces use only link-local addresses (e.g. as described in RFC5549). + if addressIP.IsLoopback() { + loopbackIP, err := netutil.ChooseBindAddressForInterface(netutil.LoopbackInterfaceName) + if err != nil { + return err + } + if loopbackIP != nil { + klog.V(4).Infof("Found active IP %v on loopback interface", loopbackIP.String()) + cfg.AdvertiseAddress = loopbackIP.String() + return nil + } + return errors.New("unable to resolve link-local addresses") + } + // This is the same logic as the API Server uses, except that if no interface is found the address is set to 0.0.0.0, which is invalid and cannot be used // for bootstrapping a cluster. ip, err := ChooseAPIServerBindAddress(addressIP) diff --git a/staging/src/k8s.io/apimachinery/pkg/util/net/interface.go b/staging/src/k8s.io/apimachinery/pkg/util/net/interface.go index daf5d249645..fa1ebb947f1 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/net/interface.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/net/interface.go @@ -36,6 +36,11 @@ const ( familyIPv6 AddressFamily = 6 ) +const ( + // LoopbackInterfaceName is the default name of the loopback interface + LoopbackInterfaceName = "lo" +) + const ( ipv4RouteFile = "/proc/net/route" ipv6RouteFile = "/proc/net/ipv6_route" @@ -414,3 +419,21 @@ func ChooseBindAddress(bindAddress net.IP) (net.IP, error) { } return bindAddress, nil } + +// ChooseBindAddressForInterface choose a global IP for a specific interface, with priority given to IPv4. +// This is required in case of network setups where default routes are present, but network +// interfaces use only link-local addresses (e.g. as described in RFC5549). +// e.g when using BGP to announce a host IP over link-local ip addresses and this ip address is attached to the lo interface. +func ChooseBindAddressForInterface(intfName string) (net.IP, error) { + var nw networkInterfacer = networkInterface{} + for _, family := range []AddressFamily{familyIPv4, familyIPv6} { + ip, err := getIPFromInterface(intfName, family, nw) + if err != nil { + return nil, err + } + if ip != nil { + return ip, nil + } + } + return nil, fmt.Errorf("unable to select an IP from %s network interface", intfName) +}