There was a bug in handling "default-allow" ipsets - it could not
cope with out of order receive of Pod updates. E.g:
1) AddPod(Pod_A, PodIP_A),
2) AddPod(Pod_B, PodIP_A),
3) DeletePod(Pod_A),
would result with PodIP_A being removed from "default-allow" ipset,
as such ipset entries were not refcounted. Bypassing of refcounting
was due to the fact that adding a netpol which dst-selects a Pod which
IP addr is in "default-allow" ipset has to remove the IP addr from
the ipset regardless of refcount value of the IP addr entry in the
ipset. So, refcounting in such case didn't make sense and it was
bypassed.
This commit changes the way entries are refcounted in ipsets, and
it makes "default-allow" ipset the same citizen as other ipsets, so
from now "default-allow" ipset is refcounted.
379 lines
11 KiB
Go
379 lines
11 KiB
Go
package npc
|
|
|
|
import (
|
|
"log"
|
|
"testing"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/weaveworks/weave/npc/ipset"
|
|
coreapi "k8s.io/api/core/v1"
|
|
networkingv1 "k8s.io/api/networking/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
)
|
|
|
|
type mockSet struct {
|
|
name ipset.Name
|
|
setType ipset.Type
|
|
subSets map[string]map[types.UID]bool
|
|
}
|
|
|
|
type mockIPSet struct {
|
|
sets map[string]mockSet
|
|
}
|
|
|
|
func newMockIPSet() mockIPSet {
|
|
return mockIPSet{sets: make(map[string]mockSet)}
|
|
}
|
|
|
|
func (i *mockIPSet) Create(ipsetName ipset.Name, ipsetType ipset.Type) error {
|
|
if _, ok := i.sets[string(ipsetName)]; ok {
|
|
return errors.Errorf("ipset %s already exists", ipsetName)
|
|
}
|
|
i.sets[string(ipsetName)] = mockSet{name: ipsetName, setType: ipsetType, subSets: make(map[string]map[types.UID]bool)}
|
|
return nil
|
|
}
|
|
|
|
func (i *mockIPSet) AddEntry(user types.UID, ipsetName ipset.Name, entry string, comment string) error {
|
|
log.Printf("adding entry %s to %s for %s", entry, ipsetName, user)
|
|
if _, ok := i.sets[string(ipsetName)]; !ok {
|
|
return errors.Errorf("%s does not exist", entry)
|
|
}
|
|
if i.sets[string(ipsetName)].subSets[entry] == nil {
|
|
i.sets[string(ipsetName)].subSets[entry] = make(map[types.UID]bool)
|
|
}
|
|
if _, ok := i.sets[string(ipsetName)].subSets[entry][user]; ok {
|
|
return errors.Errorf("user %s already owns entry %s", user, entry)
|
|
}
|
|
i.sets[string(ipsetName)].subSets[entry][user] = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *mockIPSet) DelEntry(user types.UID, ipsetName ipset.Name, entry string) error {
|
|
log.Printf("deleting entry %s from %s for %s", entry, ipsetName, user)
|
|
if _, ok := i.sets[string(ipsetName)]; !ok {
|
|
return errors.Errorf("ipset %s does not exist", ipsetName)
|
|
}
|
|
if _, ok := i.sets[string(ipsetName)].subSets[entry][user]; !ok {
|
|
return errors.Errorf("user %s does not own entry %s", user, entry)
|
|
}
|
|
delete(i.sets[string(ipsetName)].subSets[entry], user)
|
|
|
|
if len(i.sets[string(ipsetName)].subSets[entry]) == 0 {
|
|
delete(i.sets[string(ipsetName)].subSets, entry)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *mockIPSet) Exist(user types.UID, ipsetName ipset.Name, entry string) bool {
|
|
_, ok := i.sets[string(ipsetName)].subSets[entry][user]
|
|
return ok
|
|
}
|
|
|
|
func (i *mockIPSet) entryExists(ipsetName ipset.Name, entry string) bool {
|
|
return len(i.sets[string(ipsetName)].subSets[entry]) > 0
|
|
}
|
|
|
|
func (i *mockIPSet) Flush(ipsetName ipset.Name) error {
|
|
return errors.New("Not Implemented")
|
|
}
|
|
|
|
func (i *mockIPSet) FlushAll() error {
|
|
return errors.New("Not Implemented")
|
|
}
|
|
|
|
func (i *mockIPSet) Destroy(ipsetName ipset.Name) error {
|
|
if _, ok := i.sets[string(ipsetName)]; !ok {
|
|
return errors.Errorf("ipset %s does not exist", ipsetName)
|
|
}
|
|
delete(i.sets, string(ipsetName))
|
|
return nil
|
|
}
|
|
|
|
func (i *mockIPSet) DestroyAll() error {
|
|
return errors.New("Not Implemented")
|
|
}
|
|
|
|
func (i *mockIPSet) List(prefix string) ([]ipset.Name, error) {
|
|
return []ipset.Name{}, errors.New("Not Implemented")
|
|
}
|
|
|
|
type mockIPTables struct {
|
|
}
|
|
|
|
func (ipt *mockIPTables) Append(table, chain string, rulespec ...string) error {
|
|
return nil
|
|
}
|
|
|
|
func (ipt *mockIPTables) Delete(table, chain string, rulespec ...string) error {
|
|
return nil
|
|
}
|
|
|
|
func (ipt *mockIPTables) Insert(table, chain string, pos int, rulespec ...string) error {
|
|
return nil
|
|
}
|
|
|
|
func TestRegressionPolicyNamespaceOrdering3059(t *testing.T) {
|
|
// Test for race condition between namespace and networkpolicy events
|
|
// https://github.com/weaveworks/weave/issues/3059
|
|
|
|
sourceNamespace := &coreapi.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "source",
|
|
Labels: map[string]string{
|
|
"app": "source",
|
|
},
|
|
},
|
|
}
|
|
|
|
destinationNamespace := &coreapi.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "destination",
|
|
},
|
|
}
|
|
|
|
port := intstr.FromInt(12345)
|
|
|
|
networkPolicy := &networkingv1.NetworkPolicy{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "network-policy",
|
|
Namespace: "destination",
|
|
},
|
|
Spec: networkingv1.NetworkPolicySpec{
|
|
Ingress: []networkingv1.NetworkPolicyIngressRule{
|
|
{
|
|
From: []networkingv1.NetworkPolicyPeer{
|
|
{
|
|
NamespaceSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"app": "source",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Ports: []networkingv1.NetworkPolicyPort{
|
|
{
|
|
Port: &port,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Namespaces first
|
|
m := newMockIPSet()
|
|
controller := New("foo", false, &mockIPTables{}, &m)
|
|
|
|
const (
|
|
selectorIPSetName = "weave-I239Zp%sCvoVt*D6u=A!2]YEk"
|
|
sourceIPSetName = "weave-HboJG1fGgG]/SR%k9H#hv5e96"
|
|
)
|
|
|
|
controller.AddNamespace(sourceNamespace)
|
|
controller.AddNamespace(destinationNamespace)
|
|
|
|
controller.AddNetworkPolicy(networkPolicy)
|
|
|
|
require.Equal(t, true, len(m.sets[selectorIPSetName].subSets[sourceIPSetName]) > 0)
|
|
|
|
// NetworkPolicy first
|
|
m = newMockIPSet()
|
|
controller = New("foo", false, &mockIPTables{}, &m)
|
|
|
|
controller.AddNetworkPolicy(networkPolicy)
|
|
|
|
controller.AddNamespace(sourceNamespace)
|
|
controller.AddNamespace(destinationNamespace)
|
|
|
|
require.Equal(t, true, len(m.sets[selectorIPSetName].subSets[sourceIPSetName]) > 0)
|
|
}
|
|
|
|
// Tests default-allow ipset behavior when running in non-legacy mode.
|
|
func TestDefaultAllow(t *testing.T) {
|
|
const (
|
|
defaultAllowIPSetName = "weave-E.1.0W^NGSp]0_t5WwH/]gX@L"
|
|
fooPodIP = "10.32.0.10"
|
|
barPodIP = "10.32.0.11"
|
|
barPodNewIP = "10.32.0.12"
|
|
)
|
|
|
|
m := newMockIPSet()
|
|
controller := New("bar", false, &mockIPTables{}, &m)
|
|
|
|
defaultNamespace := &coreapi.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
},
|
|
}
|
|
controller.AddNamespace(defaultNamespace)
|
|
|
|
// Should create an ipset for default-allow
|
|
require.Contains(t, m.sets, defaultAllowIPSetName)
|
|
|
|
podFoo := &coreapi.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
UID: "foo",
|
|
Namespace: "default",
|
|
Name: "foo",
|
|
Labels: map[string]string{"run": "foo"}},
|
|
Status: coreapi.PodStatus{PodIP: fooPodIP}}
|
|
controller.AddPod(podFoo)
|
|
|
|
// Should add the foo pod to default-allow
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, fooPodIP))
|
|
|
|
podBar := &coreapi.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
UID: "bar",
|
|
Namespace: "default",
|
|
Name: "bar",
|
|
Labels: map[string]string{"run": "bar"}},
|
|
Status: coreapi.PodStatus{PodIP: barPodIP}}
|
|
podBarNoIP := &coreapi.Pod{ObjectMeta: podBar.ObjectMeta}
|
|
controller.AddPod(podBarNoIP)
|
|
controller.UpdatePod(podBarNoIP, podBar)
|
|
|
|
// Should add the bar pod to default-allow
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, barPodIP))
|
|
|
|
// Allow access from the bar pod to the foo pod
|
|
netpol := &networkingv1.NetworkPolicy{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "allow-from-bar-to-foo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: networkingv1.NetworkPolicySpec{
|
|
PodSelector: metav1.LabelSelector{MatchLabels: map[string]string{"run": "foo"}},
|
|
Ingress: []networkingv1.NetworkPolicyIngressRule{{
|
|
From: []networkingv1.NetworkPolicyPeer{{
|
|
PodSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{"run": "bar"},
|
|
},
|
|
}},
|
|
}},
|
|
},
|
|
}
|
|
controller.AddNetworkPolicy(netpol)
|
|
|
|
// Should remove the foo pod from default-allow as the netpol selects it
|
|
require.False(t, m.entryExists(defaultAllowIPSetName, fooPodIP))
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, barPodIP))
|
|
|
|
podBarWithNewIP := *podBar
|
|
podBarWithNewIP.Status.PodIP = barPodNewIP
|
|
controller.UpdatePod(podBar, &podBarWithNewIP)
|
|
|
|
// Should update IP addr of the bar pod in default-allow
|
|
require.False(t, m.entryExists(defaultAllowIPSetName, barPodIP))
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, barPodNewIP))
|
|
|
|
controller.UpdatePod(&podBarWithNewIP, podBarNoIP)
|
|
// Should remove the bar pod from default-allow as it does not have any IP addr
|
|
require.False(t, m.entryExists(defaultAllowIPSetName, barPodNewIP))
|
|
|
|
podFooWithNewLabel := *podFoo
|
|
podFooWithNewLabel.ObjectMeta.Labels = map[string]string{"run": "new-foo"}
|
|
controller.UpdatePod(podFoo, &podFooWithNewLabel)
|
|
|
|
// Should bring back the foo pod to default-allow as it does not match dst of any netpol
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, fooPodIP))
|
|
|
|
controller.UpdatePod(&podFooWithNewLabel, podFoo)
|
|
// Should remove from default-allow as it matches the netpol after the update
|
|
require.False(t, m.entryExists(defaultAllowIPSetName, fooPodIP))
|
|
|
|
controller.DeleteNetworkPolicy(netpol)
|
|
// Should bring back the foo pod to default-allow as no netpol selects it
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, fooPodIP))
|
|
|
|
controller.DeletePod(podFoo)
|
|
// Should remove foo pod from default-allow
|
|
require.False(t, m.entryExists(defaultAllowIPSetName, fooPodIP))
|
|
}
|
|
|
|
func TestOutOfOrderPodEvents(t *testing.T) {
|
|
const (
|
|
defaultAllowIPSetName = "weave-E.1.0W^NGSp]0_t5WwH/]gX@L"
|
|
runBarIPSetName = "weave-bZ~x=yBgzH)Ht()K*Uv3z{M]Y"
|
|
podIP = "10.32.0.10"
|
|
)
|
|
|
|
m := newMockIPSet()
|
|
controller := New("qux", false, &mockIPTables{}, &m)
|
|
|
|
defaultNamespace := &coreapi.Namespace{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
},
|
|
}
|
|
controller.AddNamespace(defaultNamespace)
|
|
|
|
podFoo := &coreapi.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
UID: "foo",
|
|
Namespace: "default",
|
|
Name: "foo",
|
|
Labels: map[string]string{"run": "foo"}},
|
|
Status: coreapi.PodStatus{PodIP: podIP}}
|
|
controller.AddPod(podFoo)
|
|
|
|
// Should be in default-allow as no netpol selects podFoo
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, podIP))
|
|
|
|
netpol := &networkingv1.NetworkPolicy{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "allow-from-bar-to-foo",
|
|
Namespace: "default",
|
|
},
|
|
Spec: networkingv1.NetworkPolicySpec{
|
|
PodSelector: metav1.LabelSelector{MatchLabels: map[string]string{"run": "foo"}},
|
|
Ingress: []networkingv1.NetworkPolicyIngressRule{{
|
|
From: []networkingv1.NetworkPolicyPeer{{
|
|
PodSelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{"run": "bar"},
|
|
},
|
|
}},
|
|
}},
|
|
},
|
|
}
|
|
controller.AddNetworkPolicy(netpol)
|
|
|
|
// Shouldn't be in default-allow as netpol above selects podFoo
|
|
require.False(t, m.entryExists(defaultAllowIPSetName, podIP))
|
|
|
|
podBar := &coreapi.Pod{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
UID: "bar",
|
|
Namespace: "default",
|
|
Name: "bar",
|
|
Labels: map[string]string{"run": "bar"}},
|
|
Status: coreapi.PodStatus{PodIP: podIP}}
|
|
controller.AddPod(podBar)
|
|
|
|
// Should be in default-allow as no netpol selects podBar
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, podIP))
|
|
require.True(t, m.Exist(podBar.ObjectMeta.UID, defaultAllowIPSetName, podIP))
|
|
// Should be in run=bar ipset
|
|
require.True(t, m.entryExists(runBarIPSetName, podIP))
|
|
|
|
controller.DeletePod(podFoo)
|
|
// Multiple duplicate events should not affect npc state
|
|
controller.DeletePod(podFoo)
|
|
controller.DeletePod(podFoo)
|
|
|
|
// Should be in default-allow as no netpol selects podBar and podFoo removal
|
|
// should not affect podBar in default-allow
|
|
require.True(t, m.entryExists(defaultAllowIPSetName, podIP))
|
|
|
|
controller.DeletePod(podBar)
|
|
|
|
// Should remove from default-allow and run=bar ipsets
|
|
require.Equal(t, 0, len(m.sets[defaultAllowIPSetName].subSets))
|
|
require.False(t, m.entryExists(runBarIPSetName, podIP))
|
|
}
|