From a8ad61b9730fcd9207f6e291cca6e8cba122f5f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan-Luis=20de=20Sousa-Valadas=20Casta=C3=B1o?= Date: Wed, 8 Jan 2025 12:07:18 +0100 Subject: [PATCH] Ensure FirstPublicAddress isn't a secondary addr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only for linux Signed-off-by: Juan-Luis de Sousa-Valadas CastaƱo --- internal/pkg/iface/iface.go | 68 ++++++++------------------- internal/pkg/iface/iface_linux.go | 77 +++++++++++++++++++++++++++++++ internal/pkg/iface/iface_other.go | 64 +++++++++++++++++++++++++ internal/pkg/iface/iface_test.go | 45 ++++++++++++++++++ 4 files changed, 205 insertions(+), 49 deletions(-) create mode 100644 internal/pkg/iface/iface_linux.go create mode 100644 internal/pkg/iface/iface_other.go create mode 100644 internal/pkg/iface/iface_test.go diff --git a/internal/pkg/iface/iface.go b/internal/pkg/iface/iface.go index ff593d898223..ae05560492fe 100644 --- a/internal/pkg/iface/iface.go +++ b/internal/pkg/iface/iface.go @@ -20,8 +20,6 @@ import ( "fmt" "net" "strings" - - "github.com/sirupsen/logrus" ) // AllAddresses returns a list of all network addresses on a node @@ -56,53 +54,25 @@ func CollectAllIPs() (addresses []net.IP, err error) { return addresses, nil } -// FirstPublicAddress return the first found non-local IPv4 address that's not part of pod network -// if any interface does not have any IPv4 address then return the first found non-local IPv6 address -func FirstPublicAddress() (string, error) { - ifs, err := net.Interfaces() - if err != nil { - return "127.0.0.1", fmt.Errorf("failed to list network interfaces: %w", err) - } - ipv6addr := "" - for _, i := range ifs { - switch { - // Skip calico CNI interface - case i.Name == "vxlan.calico": - continue - // Skip kube-router CNI interface - case i.Name == "kube-bridge": - continue - // Skip k0s CPLB interface - case i.Name == "dummyvip0": - continue - // Skip kube-router pod CNI interfaces - case strings.HasPrefix(i.Name, "veth"): - continue - // Skip calico pod CNI interfaces - case strings.HasPrefix(i.Name, "cali"): - continue - } - addresses, err := i.Addrs() - if err != nil { - logrus.Warnf("failed to get addresses for interface %s: %s", i.Name, err.Error()) - continue - } - for _, a := range addresses { - // check the address type and skip if loopback - if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - return ipnet.IP.String(), nil - } - if ipnet.IP.To16() != nil && ipv6addr == "" { - ipv6addr = ipnet.IP.String() - } - } - } - } - if ipv6addr != "" { - return ipv6addr, nil +// isPublicInterface returns true if the interface is not part of pod network. +func isPublicInterface(iface string) bool { + switch { + // Skip calico CNI interface + case iface == "vxlan.calico": + return false + // Skip kube-router CNI interface + case iface == "kube-bridge": + return false + // Skip k0s CPLB interface + case iface == "dummyvip0": + return false + // Skip kube-router pod CNI interfaces + case strings.HasPrefix(iface, "veth"): + return false + // Skip calico pod CNI interfaces + case strings.HasPrefix(iface, "cali"): + return false } - logrus.Warn("failed to find any non-local, non podnetwork addresses on host, defaulting public address to 127.0.0.1") - return "127.0.0.1", nil + return true } diff --git a/internal/pkg/iface/iface_linux.go b/internal/pkg/iface/iface_linux.go new file mode 100644 index 000000000000..8b7aecb09ca8 --- /dev/null +++ b/internal/pkg/iface/iface_linux.go @@ -0,0 +1,77 @@ +/* +Copyright 2025 k0s authors + +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 iface + +import ( + "fmt" + "net" + + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" +) + +// FirstPublicAddress return the first found non-local IPv4 address that's not part of pod network +// if any interface does not have any IPv4 address then return the first found non-local IPv6 address +func FirstPublicAddress() (string, error) { + ifs, err := net.Interfaces() + if err != nil { + return "127.0.0.1", fmt.Errorf("failed to list network interfaces: %w", err) + } + ipv6addr := "" + for _, i := range ifs { + if !isPublicInterface(i.Name) { + continue + } + + link, err := netlink.LinkByName(i.Name) + if err != nil { + logrus.WithError(err).Warnf("failed to get link by name %s", i.Name) + continue + } + + addresses, err := netlink.AddrList(link, netlink.FAMILY_ALL) + if err != nil { + logrus.WithError(err).Warnf("failed to get addresses for interface %s", i.Name) + continue + } + + for _, a := range addresses { + // skip secondary addresses. This is to avoid returning VIPs as the public address + // https://github.com/k0sproject/k0s/issues/4664 + if a.Flags&unix.IFA_F_SECONDARY != 0 { + continue + } + // check the address type and skip if loopback + if a.IPNet != nil && !a.IPNet.IP.IsLoopback() { + if a.IPNet.IP.To4() != nil { + return a.IPNet.IP.String(), nil + } + if a.IPNet.IP.To16() != nil && ipv6addr == "" { + ipv6addr = a.IPNet.IP.String() + } + } + } + + } + if ipv6addr != "" { + return ipv6addr, nil + } + + logrus.Warn("failed to find any non-local, non podnetwork addresses on host, defaulting public address to 127.0.0.1") + return "127.0.0.1", nil +} diff --git a/internal/pkg/iface/iface_other.go b/internal/pkg/iface/iface_other.go new file mode 100644 index 000000000000..9876a7c0ce15 --- /dev/null +++ b/internal/pkg/iface/iface_other.go @@ -0,0 +1,64 @@ +//go:build !linux + +/* +Copyright 2025 k0s authors + +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 iface + +import ( + "fmt" + "net" + + "github.com/sirupsen/logrus" +) + +// FirstPublicAddress return the first found non-local IPv4 address that's not part of pod network +// if any interface does not have any IPv4 address then return the first found non-local IPv6 address +func FirstPublicAddress() (string, error) { + ifs, err := net.Interfaces() + if err != nil { + return "127.0.0.1", fmt.Errorf("failed to list network interfaces: %w", err) + } + ipv6addr := "" + for _, i := range ifs { + if !isPublicInterface(i.Name) { + continue + } + + addresses, err := i.Addrs() + if err != nil { + logrus.Warnf("failed to get addresses for interface %s: %s", i.Name, err.Error()) + continue + } + for _, a := range addresses { + // check the address type and skip if loopback + if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String(), nil + } + if ipnet.IP.To16() != nil && ipv6addr == "" { + ipv6addr = ipnet.IP.String() + } + } + } + } + if ipv6addr != "" { + return ipv6addr, nil + } + + logrus.Warn("failed to find any non-local, non podnetwork addresses on host, defaulting public address to 127.0.0.1") + return "127.0.0.1", nil +} diff --git a/internal/pkg/iface/iface_test.go b/internal/pkg/iface/iface_test.go new file mode 100644 index 000000000000..8bc7ce0285c1 --- /dev/null +++ b/internal/pkg/iface/iface_test.go @@ -0,0 +1,45 @@ +/* +Copyright 2025 k0s authors + +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 iface + +import ( + "testing" +) + +func TestIsPublicInterface(t *testing.T) { + tests := []struct { + iface string + want bool + }{ + {"vxlan.calico", false}, + {"kube-bridge", false}, + {"dummyvip0", false}, + {"veth1234", false}, + {"cali1234", false}, + {"eth0", true}, + {"wlan0", true}, + {"lo", true}, + } + + for _, tt := range tests { + t.Run(tt.iface, func(t *testing.T) { + if got := isPublicInterface(tt.iface); got != tt.want { + t.Errorf("isPublicInterface(%q) = %v, want %v", tt.iface, got, tt.want) + } + }) + } +}