diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index 519c5ba115..bda02378dc 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -2,6 +2,7 @@ package kubernetes import ( "fmt" + "net" "strings" "k8s.io/apimachinery/pkg/labels" @@ -294,23 +295,32 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { return result, services, err } -// FIXME: Hideous hack to remove persistent-connection edges to virtual service -// IPs attributed to the internet. We add each service IP as a /32 network -// (the global service-cluster-ip-range is not exposed by the API -// server so we treat each IP as a /32 network see -// https://github.com/kubernetes/kubernetes/issues/25533). -// The right way of fixing this is performing DNAT mapping on persistent -// connections for which we don't have a robust solution -// (see https://github.com/weaveworks/scope/issues/1491) +// FIXME: Hideous hack to remove persistent-connection edges to +// virtual service IPs attributed to the internet. The global +// service-cluster-ip-range is not exposed by the API server (see +// https://github.com/kubernetes/kubernetes/issues/25533), so instead +// we synthesise it by computing the smallest network that contains +// all service IPs. That network may be smaller than the actual range +// but that is ok, since in the end all we care about is that it +// contains all the service IPs. +// +// The right way of fixing this is performing DNAT mapping on +// persistent connections for which we don't have a robust solution +// (see https://github.com/weaveworks/scope/issues/1491). func (r *Reporter) hostTopology(services []Service) report.Topology { - localNetworks := report.MakeStringSet() + serviceIPs := make([]net.IP, 0, len(services)) for _, service := range services { - localNetworks = localNetworks.Add(service.ClusterIP() + "/32") + if ip := net.ParseIP(service.ClusterIP()).To4(); ip != nil { + serviceIPs = append(serviceIPs, ip) + } + } + serviceNetwork := report.ContainingIPv4Network(serviceIPs) + if serviceNetwork == nil { + return report.MakeTopology() } - node := report.MakeNode(report.MakeHostNodeID(r.hostID)) - node = node.WithSets(report.MakeSets(). - Add(host.LocalNetworks, localNetworks)) - return report.MakeTopology().AddNode(node) + return report.MakeTopology().AddNode( + report.MakeNode(report.MakeHostNodeID(r.hostID)). + WithSets(report.MakeSets().Add(host.LocalNetworks, report.MakeStringSet(serviceNetwork.String())))) } func (r *Reporter) deploymentTopology(probeID string) (report.Topology, []Deployment, error) { diff --git a/report/networks.go b/report/networks.go index 4cd9d4a9b0..819219d028 100644 --- a/report/networks.go +++ b/report/networks.go @@ -105,3 +105,70 @@ func ipv4Nets(addrs []net.Addr) []*net.IPNet { } return nets } + +// ContainingIPv4Network determines the smallest network containing +// the given IPv4 addresses. When no addresses are specified, nil is +// returned. +func ContainingIPv4Network(ips []net.IP) *net.IPNet { + if len(ips) == 0 { + return nil + } + network := net.IPNet{ + IP: ips[0], + Mask: net.CIDRMask(net.IPv4len*8, net.IPv4len*8), + } + for _, ip := range ips[1:] { + network.Mask = net.CIDRMask(commonPrefixLen(network.IP, ip), net.IPv4len*8) + network.IP = network.IP.Mask(network.Mask) + } + return &network +} + +// commonPrefixLen reports the length of the longest prefix (looking +// at the most significant, or leftmost, bits) that the +// two addresses have in common, up to the length of a's prefix (i.e., +// the portion of the address not including the interface ID). +// +// If a or b is an IPv4 address as an IPv6 address, the IPv4 addresses +// are compared (with max common prefix length of 32). +// If a and b are different IP versions, 0 is returned. +// +// See https://tools.ietf.org/html/rfc6724#section-2.2 +// +// copied from https://golang.org/pkg/net/?m=all#commonPrefixLen +func commonPrefixLen(a, b net.IP) (cpl int) { + if a4 := a.To4(); a4 != nil { + a = a4 + } + if b4 := b.To4(); b4 != nil { + b = b4 + } + if len(a) != len(b) { + return 0 + } + // If IPv6, only up to the prefix (first 64 bits) + if len(a) > 8 { + a = a[:8] + b = b[:8] + } + for len(a) > 0 { + if a[0] == b[0] { + cpl += 8 + a = a[1:] + b = b[1:] + continue + } + bits := 8 + ab, bb := a[0], b[0] + for { + ab >>= 1 + bb >>= 1 + bits-- + if ab == bb { + cpl += bits + return + } + } + } + return +} diff --git a/report/networks_test.go b/report/networks_test.go index 531a5ff0e3..59337f7b0d 100644 --- a/report/networks_test.go +++ b/report/networks_test.go @@ -4,6 +4,7 @@ import ( "net" "testing" + "github.com/stretchr/testify/assert" "github.com/weaveworks/scope/report" ) @@ -23,3 +24,18 @@ func TestContains(t *testing.T) { t.Errorf("10.0.0.1 in %v", networks) } } + +func TestContainingIPv4Network(t *testing.T) { + assert.Nil(t, containingIPv4Networks([]string{})) + assert.Equal(t, "10.0.0.1/32", containingIPv4Networks([]string{"10.0.0.1"}).String()) + assert.Equal(t, "10.0.0.0/17", containingIPv4Networks([]string{"10.0.0.1", "10.0.2.55", "10.0.106.48"}).String()) + assert.Equal(t, "0.0.0.0/0", containingIPv4Networks([]string{"10.0.0.1", "192.168.0.1"}).String()) +} + +func containingIPv4Networks(ipstrings []string) *net.IPNet { + ips := make([]net.IP, len(ipstrings)) + for i, ip := range ipstrings { + ips[i] = net.ParseIP(ip) + } + return report.ContainingIPv4Network(ips) +}