Skip to content

Commit

Permalink
synthesise k8s service network from service IPs
Browse files Browse the repository at this point in the history
This prevents cluttering host.LocalNetworks with lots of /32
addresses. These were unsightly and rather distracting in the UI. They
also bloated the report and slowed down server-side rendering.

Fixes #2748.
  • Loading branch information
rade committed Jul 31, 2017
1 parent d7c1bfb commit 8a7370d
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 14 deletions.
38 changes: 24 additions & 14 deletions probe/kubernetes/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kubernetes

import (
"fmt"
"net"
"strings"

"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -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) {
Expand Down
67 changes: 67 additions & 0 deletions report/networks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 16 additions & 0 deletions report/networks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net"
"testing"

"github.com/stretchr/testify/assert"
"github.com/weaveworks/scope/report"
)

Expand All @@ -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)
}

0 comments on commit 8a7370d

Please sign in to comment.