Repository: cilium/cilium
Pull request #12787 (2020-08-05 17:10:40)

bpf,lbmap: Fix affinity v6 map and add runtime check for BPF map representation key/val sizes
Please see commit messages.

Regarding the check: I was thinking about removing the actual sizes, and deriving it from the passed interfaces instead. However, having both adds more safety.

Fixes: 5ca1f7d7c6 ("bpf: Add session affinity maps")

brb: test-me-please
coveralls: 
[![Coverage Status](https://coveralls.io/builds/32562766/badge)](https://coveralls.io/builds/32562766)

Coverage increased (+0.02%) to 36.888% when pulling **4d74722b8aa15018e94b1b654859486e80087583 on pr/brb/fix-affinity-val-size-v6** into **d80e66fd95fe07976197575abf4e982220e61388 on master**.

brb: GKE is failing due to provisioning problems:
```
11:28:23  + ./select-cluster.sh
11:28:23  selecting random cluster
11:28:24  
11:28:24  * - There is an upgrade available for your cluster(s).
11:28:24  
11:28:24  To upgrade nodes to the latest available version, run
11:28:24    $ gcloud container clusters upgrade NAME
11:28:24  selecting random cluster
```

Patch:
diff --git a/Documentation/troubleshooting.rst b/Documentation/troubleshooting.rst
index 8ab2044757..091fef9270 100644
--- a/Documentation/troubleshooting.rst
+++ b/Documentation/troubleshooting.rst
@@ -369,8 +369,10 @@ test:
    pod-to-a-allowed-cnp-87b5895c8-bfw4x                    1/1     Running   0          68s
    pod-to-a-b76ddb6b4-2v4kb                                1/1     Running   0          68s
    pod-to-a-denied-cnp-677d9f567b-kkjp4                    1/1     Running   0          68s
+   pod-to-b-intra-node-nodeport-8484fb6d89-bwj8q           1/1     Running   0          68s
    pod-to-b-multi-node-clusterip-f7655dbc8-h5bwk           1/1     Running   0          68s
    pod-to-b-multi-node-headless-5fd98b9648-5bjj8           1/1     Running   0          68s
+   pod-to-b-multi-node-nodeport-74bd8d7bd5-kmfmm           1/1     Running   0          68s
    pod-to-external-1111-7489c7c46d-jhtkr                   1/1     Running   0          68s
    pod-to-external-fqdn-allow-google-cnp-b7b6bcdcb-97p75   1/1     Running   0          68s
 
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml b/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
index 5c7cd2d5e0..90d32ec7b2 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-hostport.yaml
@@ -1380,6 +1380,132 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 ---
+metadata:
+  name: pod-to-b-multi-node-nodeport
+  labels:
+    name: pod-to-b-multi-node-nodeport
+    topology: multi-node
+    component: nodeport-check
+    quarantine: "false"
+    type: autocheck
+spec:
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-multi-node-nodeport
+    spec:
+      hostNetwork: false
+      containers:
+      - name: pod-to-b-multi-node-nodeport-container
+        ports: []
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        imagePullPolicy: IfNotPresent
+        command:
+        - /bin/ash
+        - -c
+        - sleep 1000000000
+        readinessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+        livenessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: kubernetes.io/hostname
+  selector:
+    matchLabels:
+      name: pod-to-b-multi-node-nodeport
+  replicas: 1
+apiVersion: apps/v1
+kind: Deployment
+---
+metadata:
+  name: pod-to-b-intra-node-nodeport
+  labels:
+    name: pod-to-b-intra-node-nodeport
+    topology: intra-node
+    component: nodeport-check
+    quarantine: "false"
+    type: autocheck
+spec:
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-intra-node-nodeport
+    spec:
+      hostNetwork: false
+      containers:
+      - name: pod-to-b-intra-node-nodeport-container
+        ports: []
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        imagePullPolicy: IfNotPresent
+        command:
+        - /bin/ash
+        - -c
+        - sleep 1000000000
+        readinessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+        livenessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+      affinity:
+        podAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: kubernetes.io/hostname
+  selector:
+    matchLabels:
+      name: pod-to-b-intra-node-nodeport
+  replicas: 1
+apiVersion: apps/v1
+kind: Deployment
+---
 metadata:
   name: echo-a
   labels:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml b/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
index e428331663..43704d4949 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check-single-node.yaml
@@ -419,6 +419,69 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 ---
+metadata:
+  name: pod-to-b-intra-node-nodeport
+  labels:
+    name: pod-to-b-intra-node-nodeport
+    topology: intra-node
+    component: nodeport-check
+    quarantine: "false"
+    type: autocheck
+spec:
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-intra-node-nodeport
+    spec:
+      hostNetwork: false
+      containers:
+      - name: pod-to-b-intra-node-nodeport-container
+        ports: []
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        imagePullPolicy: IfNotPresent
+        command:
+        - /bin/ash
+        - -c
+        - sleep 1000000000
+        readinessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+        livenessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+      affinity:
+        podAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: kubernetes.io/hostname
+  selector:
+    matchLabels:
+      name: pod-to-b-intra-node-nodeport
+  replicas: 1
+apiVersion: apps/v1
+kind: Deployment
+---
 metadata:
   name: echo-a
   labels:
diff --git a/examples/kubernetes/connectivity-check/connectivity-check.yaml b/examples/kubernetes/connectivity-check/connectivity-check.yaml
index 9123eeecf9..83d3d521b3 100644
--- a/examples/kubernetes/connectivity-check/connectivity-check.yaml
+++ b/examples/kubernetes/connectivity-check/connectivity-check.yaml
@@ -673,6 +673,132 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 ---
+metadata:
+  name: pod-to-b-multi-node-nodeport
+  labels:
+    name: pod-to-b-multi-node-nodeport
+    topology: multi-node
+    component: nodeport-check
+    quarantine: "false"
+    type: autocheck
+spec:
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-multi-node-nodeport
+    spec:
+      hostNetwork: false
+      containers:
+      - name: pod-to-b-multi-node-nodeport-container
+        ports: []
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        imagePullPolicy: IfNotPresent
+        command:
+        - /bin/ash
+        - -c
+        - sleep 1000000000
+        readinessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+        livenessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: kubernetes.io/hostname
+  selector:
+    matchLabels:
+      name: pod-to-b-multi-node-nodeport
+  replicas: 1
+apiVersion: apps/v1
+kind: Deployment
+---
+metadata:
+  name: pod-to-b-intra-node-nodeport
+  labels:
+    name: pod-to-b-intra-node-nodeport
+    topology: intra-node
+    component: nodeport-check
+    quarantine: "false"
+    type: autocheck
+spec:
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-intra-node-nodeport
+    spec:
+      hostNetwork: false
+      containers:
+      - name: pod-to-b-intra-node-nodeport-container
+        ports: []
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        imagePullPolicy: IfNotPresent
+        command:
+        - /bin/ash
+        - -c
+        - sleep 1000000000
+        readinessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+        livenessProbe:
+          exec:
+            command:
+            - curl
+            - -sS
+            - --fail
+            - --connect-timeout
+            - "5"
+            - -o
+            - /dev/null
+            - echo-b-host-headless:31313/public
+      affinity:
+        podAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: kubernetes.io/hostname
+  selector:
+    matchLabels:
+      name: pod-to-b-intra-node-nodeport
+  replicas: 1
+apiVersion: apps/v1
+kind: Deployment
+---
 metadata:
   name: echo-a
   labels:
diff --git a/examples/kubernetes/connectivity-check/services.cue b/examples/kubernetes/connectivity-check/services.cue
index 2e14924763..ce502f86bc 100644
--- a/examples/kubernetes/connectivity-check/services.cue
+++ b/examples/kubernetes/connectivity-check/services.cue
@@ -36,3 +36,11 @@ _hostPortDeployment: {
 }
 deployment: "pod-to-b-multi-node-hostport": _hostPortDeployment
 deployment: "pod-to-b-intra-node-hostport": _hostPortDeployment
+
+// NodePort checks
+_nodePortDeployment: {
+	metadata: labels: component: "nodeport-check"
+	_probeTarget: "echo-b-host-headless:31313"
+}
+deployment: "pod-to-b-multi-node-nodeport": _nodePortDeployment
+deployment: "pod-to-b-intra-node-nodeport": _nodePortDeployment
diff --git a/pkg/bpf/map_linux.go b/pkg/bpf/map_linux.go
index b889b648cd..79c8b7d075 100644
--- a/pkg/bpf/map_linux.go
+++ b/pkg/bpf/map_linux.go
@@ -130,6 +130,14 @@ type Map struct {
 
 // NewMap creates a new Map instance - object representing a BPF map
 func NewMap(name string, mapType MapType, mapKey MapKey, keySize int, mapValue MapValue, valueSize, maxEntries int, flags uint32, innerID uint32, dumpParser DumpParser) *Map {
+	if size := reflect.TypeOf(mapKey).Elem().Size(); size != uintptr(keySize) {
+		panic(fmt.Sprintf("Invalid %s map key size (%d != %d)", name, size, keySize))
+	}
+
+	if size := reflect.TypeOf(mapValue).Elem().Size(); size != uintptr(valueSize) {
+		panic(fmt.Sprintf("Invalid %s map value size (%d != %d)", name, size, valueSize))
+	}
+
 	m := &Map{
 		MapInfo: MapInfo{
 			MapType:       mapType,
diff --git a/pkg/checker/checker.go b/pkg/checker/checker.go
index e5f99a5484..46154bea11 100644
--- a/pkg/checker/checker.go
+++ b/pkg/checker/checker.go
@@ -15,6 +15,7 @@
 package checker
 
 import (
+	"fmt"
 	"reflect"
 	"regexp"
 
@@ -172,3 +173,42 @@ func (checker *matchesChecker) Check(params []interface{}, _ []string) (result b
 	}
 	return matches, ""
 }
+
+// -----------------------------------------------------------------------
+// HasKey checker.
+
+type hasKeyChecker struct {
+	*check.CheckerInfo
+}
+
+// The HasKey checker verifies that the obtained map contains the
+// provided key.
+//
+// For example:
+//
+//     c.Assert(myMap, HasKey, "five")
+//
+var HasKey check.Checker = &hasKeyChecker{
+	&check.CheckerInfo{Name: "HasKey", Params: []string{"map", "key"}},
+}
+
+func (checker *hasKeyChecker) Check(params []interface{}, names []string) (result bool, error string) {
+	m := reflect.ValueOf(params[0])
+	mType := m.Type()
+	key := reflect.ValueOf(params[1])
+	keyType := key.Type()
+
+	if mType.Kind() != reflect.Map {
+		return false, fmt.Sprintf("'%s' must be a map", names[0])
+	}
+	if mType.Key() != keyType {
+		return false, fmt.Sprintf("'%s' must be of '%s's key type (%s, not %s)",
+			names[1], names[0], mType.Key(), keyType)
+	}
+	for _, v := range m.MapKeys() {
+		if v.Interface() == key.Interface() {
+			return true, ""
+		}
+	}
+	return false, fmt.Sprintf("'%s' has no key %v", names[0], key.Interface())
+}
diff --git a/pkg/endpoint/bpf.go b/pkg/endpoint/bpf.go
index a94af35f3d..234c90716f 100644
--- a/pkg/endpoint/bpf.go
+++ b/pkg/endpoint/bpf.go
@@ -211,15 +211,18 @@ func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirec
 				var finalizeFunc revert.FinalizeFunc
 				var revertFunc revert.RevertFunc
 
-				proxyID, err := e.ProxyID(e.desiredPolicy.NamedPortsMap, l4)
-				if err != nil {
-					// skip redirects for which a proxyID cannot be created.
-					// Right now this happens only due to named port mapping not existing and
-					// we'll be called again when the mapping is available.
-					log.WithError(err).WithField(logfields.EndpointID, e.ID).Warning("Skipping adding redirect")
+				proxyID := e.proxyID(l4)
+				if proxyID == "" {
+					// Skip redirects for which a proxyID cannot be created.
+					// This may happen due to the named port mapping not
+					// existing or multiple PODs defining the same port name
+					// with different port values. The redirect will be created
+					// when the mapping is available or when the port name
+					// conflicts have been resolved in POD specs.
 					continue
 				}
 
+				var err error
 				redirectPort, err, finalizeFunc, revertFunc = e.proxy.CreateOrUpdateRedirect(l4, proxyID, e, proxyWaitGroup)
 				if err != nil {
 					revertStack.Revert() // Ignore errors while reverting. This is best-effort.
@@ -259,7 +262,7 @@ func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirec
 				direction = trafficdirection.Egress
 			}
 
-			keysFromFilter := l4.ToMapState(e.desiredPolicy.NamedPortsMap, direction)
+			keysFromFilter := l4.ToMapState(e, direction)
 
 			for keyFromFilter, entry := range keysFromFilter {
 				if oldEntry, ok := e.desiredPolicy.PolicyMapState[keyFromFilter]; ok {
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index 145b47988f..6860b69318 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -47,6 +47,7 @@ import (
 	slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/core/v1"
 	"github.com/cilium/cilium/pkg/labels"
 	"github.com/cilium/cilium/pkg/lock"
+	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/mac"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
@@ -224,7 +225,10 @@ type Endpoint struct {
 
 	// k8sPorts contains container ports associated in the pod.
 	// It is used to enforce k8s network policies with port names.
-	k8sPorts policy.NamedPortsMap
+	k8sPorts policy.NamedPortMap
+
+	// logLimiter rate limits potentially repeating warning logs
+	logLimiter logging.Limiter
 
 	// k8sPortsSet keep track when k8sPorts was set at least one time.
 	hasK8sMetadata bool
@@ -429,6 +433,7 @@ func createEndpoint(owner regeneration.Owner, proxy EndpointProxy, allocator cac
 		controllers:     controller.NewManager(),
 		regenFailedChan: make(chan struct{}, 1),
 		allocator:       allocator,
+		logLimiter:      logging.NewLimiter(10*time.Second, 3), // 1 log / 10 secs, burst of 3
 	}
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -1137,7 +1142,7 @@ func (e *Endpoint) SetK8sNamespace(name string) {
 // Reading the 'e.k8sPorts' member (the "map pointer") *itself* requires the endpoint lock!
 // Can't really error out as that might break backwards compatibility.
 func (e *Endpoint) SetK8sMetadata(containerPorts []slim_corev1.ContainerPort) error {
-	k8sPorts := make(policy.NamedPortsMap, len(containerPorts))
+	k8sPorts := make(policy.NamedPortMap, len(containerPorts))
 	for _, cp := range containerPorts {
 		if cp.Name == "" {
 			continue // silently skip unnamed ports
@@ -1158,6 +1163,17 @@ func (e *Endpoint) SetK8sMetadata(containerPorts []slim_corev1.ContainerPort) er
 	return nil
 }
 
+// GetK8sPorts returns the k8sPorts, which must not be modified by the caller
+func (e *Endpoint) GetK8sPorts() (k8sPorts policy.NamedPortMap, err error) {
+	err = e.rlockAlive()
+	if err != nil {
+		return nil, err
+	}
+	k8sPorts = e.k8sPorts
+	e.mutex.RUnlock()
+	return k8sPorts, nil
+}
+
 // HaveK8sMetadata returns true once hasK8sMetadata was set
 func (e *Endpoint) HaveK8sMetadata() (metadataSet bool) {
 	e.mutex.RLock()
diff --git a/pkg/endpoint/policy.go b/pkg/endpoint/policy.go
index f51941adc3..a8e5944228 100644
--- a/pkg/endpoint/policy.go
+++ b/pkg/endpoint/policy.go
@@ -16,6 +16,7 @@ package endpoint
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"strconv"
@@ -40,29 +41,89 @@ import (
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/revert"
+	"github.com/cilium/cilium/pkg/u8proto"
 
 	"github.com/sirupsen/logrus"
 )
 
-// ProxyID returns a unique string to identify a proxy mapping.
-func (e *Endpoint) ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error) {
-	return policy.ProxyIDFromFilter(e.ID, npMap, l4)
+// GetNamedPort returns the port for the given name.
+// Must be called with e.mutex NOT held
+func (e *Endpoint) GetNamedPort(ingress bool, name string, proto uint8) uint16 {
+	if ingress {
+		// Ingress only needs the ports of the POD itself
+		k8sPorts, err := e.GetK8sPorts()
+		if err != nil {
+			if e.logLimiter.Allow() {
+				e.getLogger().WithFields(logrus.Fields{
+					logfields.PortName:         name,
+					logfields.Protocol:         u8proto.U8proto(proto).String(),
+					logfields.TrafficDirection: "ingress",
+				}).WithError(err).Warning("Skipping named port")
+			}
+			return 0
+		}
+		return e.getNamedPortIngress(k8sPorts, name, proto)
+	}
+	// egress needs named ports of all the pods
+	return e.getNamedPortEgress(ipcache.IPIdentityCache.GetNamedPorts(), name, proto)
+}
+
+// GetNamedPortLocked returns port for the given name. May return an invalid (0) port
+// Must be called with e.mutex held.
+func (e *Endpoint) GetNamedPortLocked(ingress bool, name string, proto uint8) uint16 {
+	if ingress {
+		// Ingress only needs the ports of the POD itself
+		return e.getNamedPortIngress(e.k8sPorts, name, proto)
+	}
+	// egress needs named ports of all the pods
+	return e.getNamedPortEgress(ipcache.IPIdentityCache.GetNamedPorts(), name, proto)
+}
+
+func (e *Endpoint) getNamedPortIngress(npMap policy.NamedPortMap, name string, proto uint8) uint16 {
+	port, err := npMap.GetNamedPort(name, proto)
+	if err != nil && e.logLimiter.Allow() {
+		e.getLogger().WithFields(logrus.Fields{
+			logfields.PortName:         name,
+			logfields.Protocol:         u8proto.U8proto(proto).String(),
+			logfields.TrafficDirection: "ingress",
+		}).WithError(err).Warning("Skipping named port")
+	}
+	return port
+}
+
+func (e *Endpoint) getNamedPortEgress(npMap policy.NamedPortMultiMap, name string, proto uint8) uint16 {
+	port, err := npMap.GetNamedPort(name, proto)
+	// Skip logging for ErrUnknownNamedPort on egress, as the destination POD with the port name
+	// is likely not scheduled yet.
+	if err != nil && !errors.Is(err, policy.ErrUnknownNamedPort) && e.logLimiter.Allow() {
+		e.getLogger().WithFields(logrus.Fields{
+			logfields.PortName:         name,
+			logfields.Protocol:         u8proto.U8proto(proto).String(),
+			logfields.TrafficDirection: "egress",
+		}).WithError(err).Warning("Skipping named port")
+	}
+	return port
+}
+
+// proxyID returns a unique string to identify a proxy mapping.
+// Must be called with e.mutex held.
+func (e *Endpoint) proxyID(l4 *policy.L4Filter) string {
+	port := uint16(l4.Port)
+	if port == 0 && l4.PortName != "" {
+		port = e.GetNamedPortLocked(l4.Ingress, l4.PortName, uint8(l4.U8Proto))
+		if port == 0 {
+			return ""
+		}
+	}
+	return policy.ProxyID(e.ID, l4.Ingress, string(l4.Protocol), port)
 }
 
 // lookupRedirectPort returns the redirect L4 proxy port for the given L4
 // policy map key, in host byte order. Returns 0 if not found or the
 // filter doesn't require a redirect.
-// Must be called with Endpoint.Mutex held.
-func (e *Endpoint) LookupRedirectPortLocked(npMap policy.NamedPortsMap, l4Filter *policy.L4Filter) uint16 {
-	if !l4Filter.IsRedirect() {
-		return 0
-	}
-	proxyID, err := e.ProxyID(npMap, l4Filter)
-	if err != nil {
-		e.getLogger().WithError(err).Warn("ProxyID failed")
-		return 0
-	}
-	return e.realizedRedirects[proxyID]
+// Must be called with Endpoint.mutex held.
+func (e *Endpoint) LookupRedirectPortLocked(ingress bool, protocol string, port uint16) uint16 {
+	return e.realizedRedirects[policy.ProxyID(e.ID, ingress, protocol, port)]
 }
 
 // Note that this function assumes that endpoint policy has already been generated!
@@ -88,7 +149,7 @@ func (e *Endpoint) updateNetworkPolicy(proxyWaitGroup *completion.WaitGroup) (re
 	}
 
 	// Publish the updated policy to L7 proxies.
-	return e.proxy.UpdateNetworkPolicy(e, e.desiredPolicy.L4Policy, e.desiredPolicy.NamedPortsMap, e.desiredPolicy.IngressPolicyEnabled, e.desiredPolicy.EgressPolicyEnabled, proxyWaitGroup)
+	return e.proxy.UpdateNetworkPolicy(e, e.desiredPolicy.L4Policy, e.desiredPolicy.IngressPolicyEnabled, e.desiredPolicy.EgressPolicyEnabled, proxyWaitGroup)
 }
 
 func (e *Endpoint) useCurrentNetworkPolicy(proxyWaitGroup *completion.WaitGroup) {
@@ -181,7 +242,7 @@ func (e *Endpoint) regeneratePolicy() (retErr error) {
 		e.getLogger().WithError(err).Warning("Failed to update policy")
 		return err
 	}
-	calculatedPolicy := e.selectorPolicy.Consume(e, ipcache.IPIdentityCache.GetNamedPorts())
+	calculatedPolicy := e.selectorPolicy.Consume(e)
 
 	stats.policyCalculation.End(true)
 
diff --git a/pkg/endpoint/proxy.go b/pkg/endpoint/proxy.go
index 636f700b3c..f18d6fbbf8 100644
--- a/pkg/endpoint/proxy.go
+++ b/pkg/endpoint/proxy.go
@@ -27,7 +27,7 @@ import (
 type EndpointProxy interface {
 	CreateOrUpdateRedirect(l4 policy.ProxyPolicy, id string, localEndpoint logger.EndpointUpdater, wg *completion.WaitGroup) (proxyPort uint16, err error, finalizeFunc revert.FinalizeFunc, revertFunc revert.RevertFunc)
 	RemoveRedirect(id string, wg *completion.WaitGroup) (error, revert.FinalizeFunc, revert.RevertFunc)
-	UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error)
+	UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error)
 	UseCurrentNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, wg *completion.WaitGroup)
 	RemoveNetworkPolicy(ep logger.EndpointInfoSource)
 }
@@ -64,7 +64,7 @@ func (f *FakeEndpointProxy) RemoveRedirect(id string, wg *completion.WaitGroup)
 }
 
 // UpdateNetworkPolicy does nothing.
-func (f *FakeEndpointProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
+func (f *FakeEndpointProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
 	return nil, nil
 }
 
diff --git a/pkg/endpoint/regeneration/owner.go b/pkg/endpoint/regeneration/owner.go
index 561461aad4..5d7dbcac41 100644
--- a/pkg/endpoint/regeneration/owner.go
+++ b/pkg/endpoint/regeneration/owner.go
@@ -60,7 +60,6 @@ type EndpointInfoSource interface {
 	HasSidecarProxy() bool
 	ConntrackName() string
 	ConntrackNameLocked() string
-	ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error)
 	GetProxyInfoByFields() (uint64, string, string, []string, string, uint64, error)
 }
 
diff --git a/pkg/envoy/server.go b/pkg/envoy/server.go
index 8f3dd135aa..598d57e5f0 100644
--- a/pkg/envoy/server.go
+++ b/pkg/envoy/server.go
@@ -28,9 +28,7 @@ import (
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/envoy/xds"
-	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/lock"
-	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/policy/api"
@@ -1042,7 +1040,7 @@ func getWildcardNetworkPolicyRule(selectors policy.L7DataMap) *cilium.PortNetwor
 	}
 }
 
-func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, npMap policy.NamedPortsMap, policyEnforced bool) []*cilium.PortNetworkPolicy {
+func getDirectionNetworkPolicy(ep logger.EndpointUpdater, l4Policy policy.L4PolicyMap, policyEnforced bool) []*cilium.PortNetworkPolicy {
 	if !policyEnforced {
 		// Return an allow-all policy.
 		return allowAllPortNetworkPolicy
@@ -1066,10 +1064,8 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, npMap policy.NamedPo
 
 		port := uint16(l4.Port)
 		if port == 0 && l4.PortName != "" {
-			var err error
-			port, err = npMap.GetNamedPort(l4.PortName, uint8(l4.U8Proto))
-			if err != nil {
-				log.WithError(err).WithField(logfields.PortName, l4.PortName).Debug("getDirectionNetworkPolicy: Skipping named port")
+			port = ep.GetNamedPortLocked(l4.Ingress, l4.PortName, uint8(l4.U8Proto))
+			if port == 0 {
 				continue
 			}
 		}
@@ -1152,18 +1148,18 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, npMap policy.NamedPo
 }
 
 // getNetworkPolicy converts a network policy into a cilium.NetworkPolicy.
-func getNetworkPolicy(name string, id identity.NumericIdentity, conntrackName string, policy *policy.L4Policy,
-	npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool) *cilium.NetworkPolicy {
+func getNetworkPolicy(ep logger.EndpointUpdater, name string, policy *policy.L4Policy,
+	ingressPolicyEnforced, egressPolicyEnforced bool) *cilium.NetworkPolicy {
 	p := &cilium.NetworkPolicy{
 		Name:             name,
-		Policy:           uint64(id),
-		ConntrackMapName: conntrackName,
+		Policy:           uint64(ep.GetIdentityLocked()),
+		ConntrackMapName: ep.ConntrackNameLocked(),
 	}
 
 	// If no policy, deny all traffic. Otherwise, convert the policies for ingress and egress.
 	if policy != nil {
-		p.IngressPerPortPolicies = getDirectionNetworkPolicy(policy.Ingress, npMap, ingressPolicyEnforced)
-		p.EgressPerPortPolicies = getDirectionNetworkPolicy(policy.Egress, npMap, egressPolicyEnforced)
+		p.IngressPerPortPolicies = getDirectionNetworkPolicy(ep, policy.Ingress, ingressPolicyEnforced)
+		p.EgressPerPortPolicies = getDirectionNetworkPolicy(ep, policy.Egress, egressPolicyEnforced)
 	}
 
 	return p
@@ -1202,7 +1198,7 @@ func getNodeIDs(ep logger.EndpointUpdater, policy *policy.L4Policy) []string {
 // to L7 proxies.
 // When the proxy acknowledges the network policy update, it will result in
 // a subsequent call to the endpoint's OnProxyPolicyUpdate() function.
-func (s *XDSServer) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap,
+func (s *XDSServer) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy,
 	ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
@@ -1217,8 +1213,7 @@ func (s *XDSServer) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *polic
 		if ip == "" {
 			continue
 		}
-		networkPolicy := getNetworkPolicy(ip, ep.GetIdentityLocked(), ep.ConntrackNameLocked(), policy, npMap,
-			ingressPolicyEnforced, egressPolicyEnforced)
+		networkPolicy := getNetworkPolicy(ep, ip, policy, ingressPolicyEnforced, egressPolicyEnforced)
 		err := networkPolicy.Validate()
 		if err != nil {
 			return fmt.Errorf("error validating generated NetworkPolicy for %s: %s", ip, err), nil
diff --git a/pkg/ipcache/ipcache.go b/pkg/ipcache/ipcache.go
index 396124d470..73c68542cc 100644
--- a/pkg/ipcache/ipcache.go
+++ b/pkg/ipcache/ipcache.go
@@ -23,7 +23,6 @@ import (
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/source"
-	"github.com/cilium/cilium/pkg/u8proto"
 	"github.com/sirupsen/logrus"
 )
 
@@ -62,7 +61,7 @@ type K8sMetadata struct {
 	// PodName is the Kubernetes pod name behind the IP
 	PodName string
 	// NamedPorts is the set of named ports for the pod
-	NamedPorts policy.NamedPortsMap
+	NamedPorts policy.NamedPortMap
 }
 
 // IPCache is a collection of mappings:
@@ -83,7 +82,18 @@ type IPCache struct {
 
 	listeners []IPIdentityMappingListener
 
-	namedPorts policy.NamedPortsMap
+	// needNamedPorts is initially 'false', but will be changd to 'true' when the
+	// clusterwide named port mappings are needed for network policy computation
+	// for the first time. This avoids the overhead of maintaining 'namedPorts' map
+	// when it is known not to be needed.
+	// Protected by 'mutex'.
+	needNamedPorts bool
+
+	// namedPorts is a collection of all named ports in the cluster. This is needed
+	// only if an egress policy refers to a port by name.
+	// This map is returned to users so all updates must be made into a fresh map that
+	// is then swapped in place while 'mutex' is being held.
+	namedPorts policy.NamedPortMultiMap
 }
 
 // NewIPCache returns a new IPCache with the mappings of endpoint IP to security
@@ -177,49 +187,22 @@ func (ipc *IPCache) getK8sMetadata(ip string) *K8sMetadata {
 	return nil
 }
 
-func (ipc *IPCache) updateNamedPorts(newIP string, newNamedPorts policy.NamedPortsMap) (namedPortsChanged bool) {
+// updateNamedPorts accumulates named ports from all K8sMetadata entries to a single map
+func (ipc *IPCache) updateNamedPorts() (namedPortsChanged bool) {
+	if !ipc.needNamedPorts {
+		return false
+	}
 	// Collect new named Ports
-	npm := make(policy.NamedPortsMap, len(ipc.namedPorts))
-	for ip, km := range ipc.ipToK8sMetadata {
-		for name, value := range km.NamedPorts {
-			if ip != newIP {
-				// Check if there is a conflict between any old named port from other IPs
-				// with any of the newly added (or changed) values
-				if newValue, exists := newNamedPorts[name]; exists && newValue != value {
-					scopedLog := log.WithFields(logrus.Fields{
-						logfields.IPAddr:   newIP,
-						logfields.Identity: ipc.ipToIdentityCache[newIP],
-					})
-					newMeta := ipc.ipToK8sMetadata[newIP]
-					scopedLog = scopedLog.WithFields(logrus.Fields{
-						logfields.K8sPodName:   newMeta.PodName,
-						logfields.K8sNamespace: newMeta.Namespace,
-						logfields.NamedPorts:   newMeta.NamedPorts,
-					})
-					oldMeta := ipc.ipToK8sMetadata[ip]
-					scopedLog = scopedLog.WithFields(logrus.Fields{
-						"other-" + logfields.K8sPodName:   oldMeta.PodName,
-						"other-" + logfields.K8sNamespace: oldMeta.Namespace,
-						"other-" + logfields.NamedPorts:   oldMeta.NamedPorts,
-					})
-					scopedLog.Warningf("Conflicting named port definition: %s: %d/%s != %d/%s", name,
-						newValue.Port, u8proto.U8proto(newValue.Proto).String(),
-						value.Port, u8proto.U8proto(value.Proto).String())
-				}
-			}
-			if _, done := npm[name]; done {
-				// Do not process duplicate values
-				continue
+	npm := make(policy.NamedPortMultiMap, len(ipc.namedPorts))
+	for _, km := range ipc.ipToK8sMetadata {
+		for name, port := range km.NamedPorts {
+			if npm[name] == nil {
+				npm[name] = make(policy.PortProtoSet)
 			}
-			if oldValue, ok := ipc.namedPorts[name]; !ok || oldValue != value {
-				namedPortsChanged = true
-			}
-			npm[name] = value
+			npm[name][port] = struct{}{}
 		}
 	}
-	if len(npm) != len(ipc.namedPorts) {
-		namedPortsChanged = true
-	}
+	namedPortsChanged = !npm.Equal(ipc.namedPorts)
 	if namedPortsChanged {
 		// swap the new map in
 		if len(npm) == 0 {
@@ -241,7 +224,7 @@ func (ipc *IPCache) updateNamedPorts(newIP string, newNamedPorts policy.NamedPor
 // nil) and is propagated to the listeners. k8sMeta contains Kubernetes-specific
 // metadata such as pod namespace and pod name belonging to the IP (may be nil).
 func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8sMetadata, newIdentity Identity) (updated bool, namedPortsChanged bool) {
-	var newNamedPorts policy.NamedPortsMap
+	var newNamedPorts policy.NamedPortMap
 	if k8sMeta != nil {
 		newNamedPorts = k8sMeta.NamedPorts
 	}
@@ -382,7 +365,7 @@ func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8s
 		if namedPortsChanged {
 			// It is possible that some other POD defines same values, check if
 			// anything changes over all the PODs.
-			namedPortsChanged = ipc.updateNamedPorts(ip, newNamedPorts)
+			namedPortsChanged = ipc.updateNamedPorts()
 		}
 	}
 
@@ -490,7 +473,7 @@ func (ipc *IPCache) deleteLocked(ip string, source source.Source) (namedPortsCha
 	// Update named ports
 	namedPortsChanged = false
 	if oldK8sMeta != nil && len(oldK8sMeta.NamedPorts) > 0 {
-		namedPortsChanged = ipc.updateNamedPorts(ip, nil)
+		namedPortsChanged = ipc.updateNamedPorts()
 	}
 
 	if callbackListeners {
@@ -504,8 +487,12 @@ func (ipc *IPCache) deleteLocked(ip string, source source.Source) (namedPortsCha
 }
 
 // GetNamedPorts returns a copy of the named ports map. May return nil.
-func (ipc *IPCache) GetNamedPorts() (npm policy.NamedPortsMap) {
+func (ipc *IPCache) GetNamedPorts() (npm policy.NamedPortMultiMap) {
 	ipc.mutex.Lock()
+	if !ipc.needNamedPorts {
+		ipc.needNamedPorts = true
+		ipc.updateNamedPorts()
+	}
 	// Caller can keep using the map after the lock is released, as the map is never changed
 	// once published.
 	npm = ipc.namedPorts
diff --git a/pkg/ipcache/kvstore.go b/pkg/ipcache/kvstore.go
index d132426f3d..20670cc778 100644
--- a/pkg/ipcache/kvstore.go
+++ b/pkg/ipcache/kvstore.go
@@ -108,7 +108,7 @@ func newKVReferenceCounter(s store) *kvReferenceCounter {
 // UpsertIPToKVStore updates / inserts the provided IP->Identity mapping into the
 // kvstore, which will subsequently trigger an event in NewIPIdentityWatcher().
 func UpsertIPToKVStore(ctx context.Context, IP, hostIP net.IP, ID identity.NumericIdentity, key uint8,
-	metadata, k8sNamespace, k8sPodName string, npm policy.NamedPortsMap) error {
+	metadata, k8sNamespace, k8sPodName string, npm policy.NamedPortMap) error {
 	// Sort named ports into a slice
 	namedPorts := make([]identity.NamedPort, 0, len(npm))
 	for name, value := range npm {
@@ -291,7 +291,7 @@ restart:
 					k8sMeta = &K8sMetadata{
 						Namespace:  ipIDPair.K8sNamespace,
 						PodName:    ipIDPair.K8sPodName,
-						NamedPorts: make(policy.NamedPortsMap, len(ipIDPair.NamedPorts)),
+						NamedPorts: make(policy.NamedPortMap, len(ipIDPair.NamedPorts)),
 					}
 					for _, np := range ipIDPair.NamedPorts {
 						err = k8sMeta.NamedPorts.AddPort(np.Name, int(np.Port), np.Protocol)
diff --git a/pkg/k8s/watchers/cilium_endpoint.go b/pkg/k8s/watchers/cilium_endpoint.go
index 6877aea4fe..c66f1f54a8 100644
--- a/pkg/k8s/watchers/cilium_endpoint.go
+++ b/pkg/k8s/watchers/cilium_endpoint.go
@@ -141,16 +141,16 @@ func (k *K8sWatcher) endpointUpdated(endpoint *types.CiliumEndpoint) {
 		k8sMeta := &ipcache.K8sMetadata{
 			Namespace:  endpoint.Namespace,
 			PodName:    endpoint.Name,
-			NamedPorts: make(policy.NamedPortsMap, len(endpoint.NamedPorts)),
+			NamedPorts: make(policy.NamedPortMap, len(endpoint.NamedPorts)),
 		}
 		for _, port := range endpoint.NamedPorts {
 			p, err := u8proto.ParseProtocol(port.Protocol)
 			if err != nil {
 				continue
 			}
-			k8sMeta.NamedPorts[port.Name] = policy.NamedPort{
-				Proto: uint8(p),
+			k8sMeta.NamedPorts[port.Name] = policy.PortProto{
 				Port:  uint16(port.Port),
+				Proto: uint8(p),
 			}
 		}
 
diff --git a/pkg/k8s/watchers/pod.go b/pkg/k8s/watchers/pod.go
index 96bf369cc5..3a2865df95 100644
--- a/pkg/k8s/watchers/pod.go
+++ b/pkg/k8s/watchers/pod.go
@@ -608,11 +608,11 @@ func (k *K8sWatcher) updatePodHostData(pod *slim_corev1.Pod) (bool, error) {
 				return true, fmt.Errorf("ContainerPort: invalid port: %d", port.ContainerPort)
 			}
 			if k8sMeta.NamedPorts == nil {
-				k8sMeta.NamedPorts = make(policy.NamedPortsMap)
+				k8sMeta.NamedPorts = make(policy.NamedPortMap)
 			}
-			k8sMeta.NamedPorts[port.Name] = policy.NamedPort{
-				Proto: uint8(p),
+			k8sMeta.NamedPorts[port.Name] = policy.PortProto{
 				Port:  uint16(port.ContainerPort),
+				Proto: uint8(p),
 			}
 		}
 	}
diff --git a/pkg/logging/limiter.go b/pkg/logging/limiter.go
new file mode 100644
index 0000000000..c7578838b2
--- /dev/null
+++ b/pkg/logging/limiter.go
@@ -0,0 +1,47 @@
+// Copyright 2020 Authors of Cilium
+//
+// 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 logging
+
+import (
+	"time"
+
+	"golang.org/x/time/rate"
+)
+
+// Limiter is a wrapper around rate.Limiter that does not panic when
+// the limiter is uninitialized. The wrapping also allows more logging
+// specific functionality to be added later without changing all the call
+// sites.
+type Limiter struct {
+	bucket *rate.Limiter
+}
+
+// NewLimiter returns a new Limiter allowing log messages to be
+// emitted on average once every 'interval' and upto 'burst' messages
+// during any 'interval'.
+func NewLimiter(interval time.Duration, burst int) Limiter {
+	return Limiter{
+		bucket: rate.NewLimiter(rate.Every(interval), burst),
+	}
+}
+
+// Allow returns true if the log message is allowed under the
+// configured rate limit.
+func (ll Limiter) Allow() bool {
+	if ll.bucket == nil {
+		return true // limiter not initialized => no limit
+	}
+	return ll.bucket.Allow()
+}
diff --git a/pkg/maps/lbmap/affinity.go b/pkg/maps/lbmap/affinity.go
index 1d303c19d4..6defac61fc 100644
--- a/pkg/maps/lbmap/affinity.go
+++ b/pkg/maps/lbmap/affinity.go
@@ -61,10 +61,10 @@ var (
 		bpf.ConvertKeyValue,
 	)
 	Affinity6Map = bpf.NewMap(
-		Affinity4MapName,
+		Affinity6MapName,
 		bpf.MapTypeLRUHash,
 		&Affinity6Key{},
-		int(unsafe.Sizeof(Affinity4Key{})),
+		int(unsafe.Sizeof(Affinity6Key{})),
 		&AffinityValue{},
 		int(unsafe.Sizeof(AffinityValue{})),
 		MaxEntries,
diff --git a/pkg/policy/distillery.go b/pkg/policy/distillery.go
index 558337c8e7..294afdd534 100644
--- a/pkg/policy/distillery.go
+++ b/pkg/policy/distillery.go
@@ -30,7 +30,7 @@ import (
 type SelectorPolicy interface {
 	// Consume returns the policy in terms of connectivity to peer
 	// Identities.
-	Consume(owner PolicyOwner, npMap NamedPortsMap) *EndpointPolicy
+	Consume(owner PolicyOwner) *EndpointPolicy
 }
 
 // PolicyCache represents a cache of resolved policies for identities.
@@ -205,10 +205,10 @@ func (cip *cachedSelectorPolicy) setPolicy(policy *selectorPolicy) {
 //
 // This denotes that a particular endpoint is 'consuming' the policy from the
 // selector policy cache.
-func (cip *cachedSelectorPolicy) Consume(owner PolicyOwner, npMap NamedPortsMap) *EndpointPolicy {
+func (cip *cachedSelectorPolicy) Consume(owner PolicyOwner) *EndpointPolicy {
 	// TODO: This currently computes the EndpointPolicy from SelectorPolicy
 	// on-demand, however in future the cip is intended to cache the
 	// EndpointPolicy for this Identity and emit datapath deltas instead.
 	isHost := cip.identity.ID == identityPkg.ReservedIdentityHost
-	return cip.getPolicy().DistillPolicy(owner, npMap, isHost)
+	return cip.getPolicy().DistillPolicy(owner, isHost)
 }
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index 008cae9332..ca8f8e8f78 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -1,4 +1,4 @@
-// Copyright 2016-2019 Authors of Cilium
+// Copyright 2016-2020 Authors of Cilium
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@ import (
 	"reflect"
 	"sort"
 	"strconv"
-	"strings"
 	"sync/atomic"
 	"unsafe"
 
@@ -244,71 +243,6 @@ func (l4 *L4Filter) GetPort() uint16 {
 	return uint16(l4.Port)
 }
 
-// NamedPort represents a mapping from a port name to the port number and protocol
-type NamedPort struct {
-	Port  uint16 // non-0
-	Proto uint8  // 0 for any
-}
-
-type NamedPortsMap map[string]NamedPort
-
-func ValidatePortName(name string) (string, error) {
-	if !iana.IsSvcName(name) { // Port names are formatted as IANA Service Names
-		return "", fmt.Errorf("Invalid port name \"%s\", not using as a named port", name)
-	}
-	return strings.ToLower(name), nil // Normalize for case-insensitive comparison
-}
-
-func ParsePortProtocol(port int, protocol string) (np NamedPort, err error) {
-	var u8p u8proto.U8proto
-	if protocol == "" {
-		u8p = u8proto.TCP // K8s ContainerPort protocol defaults to TCP
-	} else {
-		var err error
-		u8p, err = u8proto.ParseProtocol(protocol)
-		if err != nil {
-			return np, fmt.Errorf("Invalid protocol \"%s\": %s", protocol, err)
-		}
-	}
-	if port < 1 || port > 65535 {
-		return np, fmt.Errorf("Port number %d out of 16-bit range", port)
-	}
-	return NamedPort{
-		Proto: uint8(u8p),
-		Port:  uint16(port),
-	}, nil
-}
-
-func (npm NamedPortsMap) AddPort(name string, port int, protocol string) error {
-	name, err := ValidatePortName(name)
-	if err != nil {
-		return err
-	}
-	np, err := ParsePortProtocol(port, protocol)
-	if err != nil {
-		return err
-	}
-	npm[name] = np
-	return nil
-}
-
-func (npm NamedPortsMap) GetNamedPort(name string, proto uint8) (port uint16, err error) {
-	if npm == nil {
-		return 0, fmt.Errorf("nil map")
-	}
-	np, ok := npm[name]
-	if !ok {
-		return 0, fmt.Errorf("named port %s not found in %v", name, npm)
-	}
-	if np.Proto != 0 && proto != np.Proto {
-		return 0, fmt.Errorf("incompatible proto")
-	}
-	if np.Port == 0 {
-		return 0, fmt.Errorf("named port has zero value")
-	}
-	return np.Port, nil
-}
-
 // ToMapState converts filter into a MapState with two possible values:
 // - Entry with ProxyPort = 0: No proxy redirection is needed for this key
 // - Entry with any other port #: Proxy redirection is required for this key,
@@ -317,7 +251,8 @@ func (npm NamedPortsMap) GetNamedPort(name string, proto uint8) (port uint16, er
 // Note: It is possible for two selectors to select the same security ID.
 // To give priority for L7 redirection (e.g., for visibility purposes), we use
 // RedirectPreferredInsert() instead of directly inserting the value to the map.
-func (l4 *L4Filter) ToMapState(npMap NamedPortsMap, direction trafficdirection.TrafficDirection) MapState {
+// PolicyOwner (aka Endpoint) is locked during this call.
+func (l4 *L4Filter) ToMapState(policyOwner PolicyOwner, direction trafficdirection.TrafficDirection) MapState {
 	port := uint16(l4.Port)
 	proto := uint8(l4.U8Proto)
 
@@ -335,10 +270,8 @@ func (l4 *L4Filter) ToMapState(npMap NamedPortsMap, direction trafficdirection.T
 
 	// resolve named port
 	if port == 0 && l4.PortName != "" {
-		var err error
-		port, err = npMap.GetNamedPort(l4.PortName, proto)
-		if err != nil {
-			logger.Debugf("ToMapState: Skipping named port: %s", err)
+		port = policyOwner.GetNamedPortLocked(l4.Ingress, l4.PortName, proto)
+		if port == 0 {
 			return keysToAdd
 		}
 	}
@@ -838,6 +771,8 @@ type L4Policy struct {
 
 	// Endpoint policies using this L4Policy
 	// These are circular references, cleaned up in Detach()
+	// This mutex is taken while Endpoint mutex is held, so Endpoint lock
+	// MUST always be taken before this mutex.
 	mutex lock.RWMutex
 	users map[*EndpointPolicy]struct{}
 }
@@ -884,30 +819,26 @@ func (l4 *L4Policy) AccumulateMapChanges(adds, deletes []identity.NumericIdentit
 	proto := uint8(l4Filter.U8Proto)
 	derivedFrom := l4Filter.DerivedFromRules
 
+	// Must take a copy of 'users' as GetNamedPort() will lock the Endpoint below and
+	// the Endpoint lock may not be taken while 'l4.mutex' is held.
 	l4.mutex.RLock()
-	for epPolicy := range l4.users {
+	users := make(map[*EndpointPolicy]struct{}, len(l4.users))
+	for user := range l4.users {
+		users[user] = struct{}{}
+	}
+	l4.mutex.RUnlock()
+
+	for epPolicy := range users {
 		// resolve named port
 		if port == 0 && l4Filter.PortName != "" {
-			var err error
-			port, err = epPolicy.NamedPortsMap.GetNamedPort(l4Filter.PortName, proto)
-			if err != nil {
-				if option.Config.Debug {
-					logger := log.WithFields(logrus.Fields{
-						logfields.Port:             port,
-						logfields.PortName:         l4Filter.PortName,
-						logfields.Protocol:         proto,
-						logfields.TrafficDirection: direction,
-						logfields.EndpointID:       epPolicy.PolicyOwner.GetID(),
-					})
-					logger.WithError(err).Debug("AccumulateMapChanges: Skipping named port")
-				}
+			port = epPolicy.PolicyOwner.GetNamedPort(direction == trafficdirection.Ingress, l4Filter.PortName, proto)
+			if port == 0 {
 				continue
 			}
 		}
 
 		epPolicy.policyMapChanges.AccumulateMapChanges(adds, deletes, port, proto, direction, redirect, derivedFrom)
 	}
-	l4.mutex.RUnlock()
 }
 
 // Detach makes the L4Policy ready for garbage collection, removing
diff --git a/pkg/policy/portmap.go b/pkg/policy/portmap.go
new file mode 100644
index 0000000000..d196c575d7
--- /dev/null
+++ b/pkg/policy/portmap.go
@@ -0,0 +1,174 @@
+// Copyright 2020 Authors of Cilium
+//
+// 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 policy
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/cilium/cilium/pkg/iana"
+	"github.com/cilium/cilium/pkg/u8proto"
+)
+
+var (
+	ErrNilMap               = errors.New("nil map")
+	ErrUnknownNamedPort     = errors.New("unknown named port")
+	ErrIncompatibleProtocol = errors.New("incompatible protocol")
+	ErrNamedPortIsZero      = errors.New("named port is zero")
+	ErrDuplicateNamedPorts  = errors.New("duplicate named ports")
+)
+
+// PortProto is a pair of port number and protocol and is used as the
+// value type in named port maps.
+type PortProto struct {
+	Port  uint16 // non-0
+	Proto uint8  // 0 for any
+}
+
+// NamedPortMap maps port names to port numbers and protocols.
+type NamedPortMap map[string]PortProto
+
+// PortProtoSet is a set of unique PortProto values.
+type PortProtoSet map[PortProto]struct{}
+
+// Equal returns true if the PortProtoSets are equal.
+func (pps PortProtoSet) Equal(other PortProtoSet) bool {
+	if len(pps) != len(other) {
+		return false
+	}
+
+	for port := range pps {
+		if _, exists := other[port]; !exists {
+			return false
+		}
+	}
+	return true
+}
+
+// NamedPortMultiMap may have multiple entries for a name if multiple PODs
+// define the same name with different values.
+type NamedPortMultiMap map[string]PortProtoSet
+
+// Equal returns true if the NamedPortMultiMaps are equal.
+func (npm NamedPortMultiMap) Equal(other NamedPortMultiMap) bool {
+	if len(npm) != len(other) {
+		return false
+	}
+	for name, ports := range npm {
+		if otherPorts, exists := other[name]; !exists || !ports.Equal(otherPorts) {
+			return false
+		}
+	}
+	return true
+}
+
+// ValidatePortName checks that the port name conforms to the IANA Service Names spec
+// and converts the port name to lower case for case-insensitive comparisons.
+func ValidatePortName(name string) (string, error) {
+	if !iana.IsSvcName(name) { // Port names are formatted as IANA Service Names
+		return "", fmt.Errorf("Invalid port name \"%s\", not using as a named port", name)
+	}
+	return strings.ToLower(name), nil // Normalize for case-insensitive comparison
+}
+
+func newPortProto(port int, protocol string) (pp PortProto, err error) {
+	var u8p u8proto.U8proto
+	if protocol == "" {
+		u8p = u8proto.TCP // K8s ContainerPort protocol defaults to TCP
+	} else {
+		var err error
+		u8p, err = u8proto.ParseProtocol(protocol)
+		if err != nil {
+			return pp, err
+		}
+	}
+	if port < 1 || port > 65535 {
+		if port == 0 {
+			return pp, ErrNamedPortIsZero
+		}
+		return pp, fmt.Errorf("Port number %d out of 16-bit range", port)
+	}
+	return PortProto{
+		Proto: uint8(u8p),
+		Port:  uint16(port),
+	}, nil
+}
+
+// AddPort adds a new PortProto to the NamedPortMap
+func (npm NamedPortMap) AddPort(name string, port int, protocol string) error {
+	name, err := ValidatePortName(name)
+	if err != nil {
+		return err
+	}
+	pp, err := newPortProto(port, protocol)
+	if err != nil {
+		return err
+	}
+	npm[name] = pp
+	return nil
+}
+
+// GetNamedPort returns the port number for the named port, if any.
+func (npm NamedPortMap) GetNamedPort(name string, proto uint8) (uint16, error) {
+	if npm == nil {
+		return 0, ErrNilMap
+	}
+	pp, ok := npm[name]
+	if !ok {
+		return 0, ErrUnknownNamedPort
+	}
+	if pp.Proto != 0 && proto != pp.Proto {
+		return 0, ErrIncompatibleProtocol
+	}
+	if pp.Port == 0 {
+		return 0, ErrNamedPortIsZero
+	}
+	return pp.Port, nil
+}
+
+// GetNamedPort returns the port number for the named port, if any.
+func (npm NamedPortMultiMap) GetNamedPort(name string, proto uint8) (uint16, error) {
+	if npm == nil {
+		return 0, ErrNilMap
+	}
+	pps, ok := npm[name]
+	if !ok {
+		// Return an error the caller can filter out as this happens only for egress policy
+		// and it is likely the destination POD with the port name is simply not scheduled yet.
+		return 0, ErrUnknownNamedPort
+	}
+	// Find if there is a single port that has no proto conflict and no zero port value
+	port := uint16(0)
+	err := ErrUnknownNamedPort
+	for pp := range pps {
+		if pp.Proto != 0 && proto != pp.Proto {
+			err = ErrIncompatibleProtocol
+			continue // conflicting proto
+		}
+		if pp.Port == 0 {
+			err = ErrNamedPortIsZero
+			continue // zero port
+		}
+		if port != 0 && pp.Port != port {
+			return 0, ErrDuplicateNamedPorts
+		}
+		port = pp.Port
+	}
+	if port == 0 {
+		return 0, err
+	}
+	return port, nil
+}
diff --git a/pkg/policy/proxyid.go b/pkg/policy/proxyid.go
index 8f4ec8ee99..b894f063bb 100644
--- a/pkg/policy/proxyid.go
+++ b/pkg/policy/proxyid.go
@@ -37,18 +37,6 @@ func ProxyIDFromKey(endpointID uint16, key Key) string {
 	return ProxyID(endpointID, key.TrafficDirection == trafficdirection.Ingress.Uint8(), u8proto.U8proto(key.Nexthdr).String(), key.DestPort)
 }
 
-// ProxyIDFromFilter returns a unique string to identify a proxy mapping.
-func ProxyIDFromFilter(endpointID uint16, npMap NamedPortsMap, l4 *L4Filter) (id string, err error) {
-	port := uint16(l4.Port)
-	if port == 0 && l4.PortName != "" {
-		port, err = npMap.GetNamedPort(l4.PortName, uint8(l4.U8Proto))
-		if err != nil {
-			return "", err
-		}
-	}
-	return ProxyID(endpointID, l4.Ingress, string(l4.Protocol), port), nil
-}
-
 // ParseProxyID parses a proxy ID returned by ProxyID and returns its components.
 func ParseProxyID(proxyID string) (endpointID uint16, ingress bool, protocol string, port uint16, err error) {
 	comps := strings.Split(proxyID, ":")
diff --git a/pkg/policy/resolve.go b/pkg/policy/resolve.go
index 93f1fd902f..f8196af297 100644
--- a/pkg/policy/resolve.go
+++ b/pkg/policy/resolve.go
@@ -57,10 +57,6 @@ type EndpointPolicy struct {
 	// referring to a shared selectorPolicy!
 	*selectorPolicy
 
-	// NamedPortsMap maps PortNames in L4Filters to port numbers. This mapping
-	// is endpoint specific. Can be nil!
-	NamedPortsMap NamedPortsMap
-
 	// PolicyMapState contains the state of this policy as it relates to the
 	// datapath. In the future, this will be factored out of this object to
 	// decouple the policy as it relates to the datapath vs. its userspace
@@ -80,7 +76,9 @@ type EndpointPolicy struct {
 // PolicyOwner is anything which consumes a EndpointPolicy.
 type PolicyOwner interface {
 	GetID() uint64
-	LookupRedirectPortLocked(npMap NamedPortsMap, l4 *L4Filter) uint16
+	LookupRedirectPortLocked(ingress bool, protocol string, port uint16) uint16
+	GetNamedPort(ingress bool, name string, proto uint8) uint16
+	GetNamedPortLocked(ingress bool, name string, proto uint8) uint16
 }
 
 // newSelectorPolicy returns an empty selectorPolicy stub.
@@ -114,10 +112,9 @@ func (p *selectorPolicy) Detach() {
 //
 // Must be performed while holding the Repository lock.
 // PolicyOwner (aka Endpoint) is also locked during this call.
-func (p *selectorPolicy) DistillPolicy(policyOwner PolicyOwner, npMap NamedPortsMap, isHost bool) *EndpointPolicy {
+func (p *selectorPolicy) DistillPolicy(policyOwner PolicyOwner, isHost bool) *EndpointPolicy {
 	calculatedPolicy := &EndpointPolicy{
 		selectorPolicy: p,
-		NamedPortsMap:  npMap,
 		PolicyMapState: make(MapState),
 		PolicyOwner:    policyOwner,
 	}
@@ -165,13 +162,15 @@ func (p *EndpointPolicy) computeDirectionL4PolicyMapEntries(l4PolicyMap L4Policy
 	for _, filter := range l4PolicyMap {
 		lookupDone := false
 		proxyport := uint16(0)
-		keysFromFilter := filter.ToMapState(p.NamedPortsMap, direction)
+		keysFromFilter := filter.ToMapState(p.PolicyOwner, direction)
 		for keyFromFilter, entry := range keysFromFilter {
 			// Fix up the proxy port for entries that need proxy redirection
 			if entry.IsRedirectEntry() {
 				if !lookupDone {
 					// only lookup once for each filter
-					proxyport = p.PolicyOwner.LookupRedirectPortLocked(p.NamedPortsMap, filter)
+					// Use 'destPort' from the key as it is already resolved
+					// from a named port if needed.
+					proxyport = p.PolicyOwner.LookupRedirectPortLocked(filter.Ingress, string(filter.Protocol), keyFromFilter.DestPort)
 					lookupDone = true
 				}
 				entry.ProxyPort = proxyport
diff --git a/pkg/proxy/logger/epinfo.go b/pkg/proxy/logger/epinfo.go
index a41d43aea5..41e1c94bd0 100644
--- a/pkg/proxy/logger/epinfo.go
+++ b/pkg/proxy/logger/epinfo.go
@@ -18,7 +18,6 @@ import (
 	"net"
 
 	"github.com/cilium/cilium/pkg/identity"
-	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/proxy/accesslog"
 )
 
@@ -39,7 +38,7 @@ type EndpointInfoSource interface {
 	// implementation.
 	ConntrackName() string
 	ConntrackNameLocked() string
-	ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error)
+	GetNamedPortLocked(ingress bool, name string, proto uint8) uint16
 	GetProxyInfoByFields() (uint64, string, string, []string, string, uint64, error)
 }
 
diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go
index 0205259854..0ad4bb7e5f 100644
--- a/pkg/proxy/proxy.go
+++ b/pkg/proxy/proxy.go
@@ -644,8 +644,8 @@ func (p *Proxy) updateRedirectMetrics() {
 }
 
 // UpdateNetworkPolicy must update the redirect configuration of an endpoint in the proxy
-func (p *Proxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
-	return p.XDSServer.UpdateNetworkPolicy(ep, policy, npMap, ingressPolicyEnforced, egressPolicyEnforced, wg)
+func (p *Proxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
+	return p.XDSServer.UpdateNetworkPolicy(ep, policy, ingressPolicyEnforced, egressPolicyEnforced, wg)
 }
 
 // UseCurrentNetworkPolicy inserts a Completion to the WaitGroup if the current network policy has not yet been acked


Test patch:
diff --git a/Documentation/gettingstarted/k8s-install-connectivity-test.rst b/Documentation/gettingstarted/k8s-install-connectivity-test.rst
index 43ed6310b2..faab932b57 100644
--- a/Documentation/gettingstarted/k8s-install-connectivity-test.rst
+++ b/Documentation/gettingstarted/k8s-install-connectivity-test.rst
@@ -25,8 +25,10 @@ indicates success or failure of the test:
    pod-to-a-allowed-cnp-87b5895c8-bfw4x                    1/1     Running   0          68s
    pod-to-a-b76ddb6b4-2v4kb                                1/1     Running   0          68s
    pod-to-a-denied-cnp-677d9f567b-kkjp4                    1/1     Running   0          68s
+   pod-to-b-intra-node-nodeport-8484fb6d89-bwj8q           1/1     Running   0          68s
    pod-to-b-multi-node-clusterip-f7655dbc8-h5bwk           1/1     Running   0          68s
    pod-to-b-multi-node-headless-5fd98b9648-5bjj8           1/1     Running   0          68s
+   pod-to-b-multi-node-nodeport-74bd8d7bd5-kmfmm           1/1     Running   0          68s
    pod-to-external-1111-7489c7c46d-jhtkr                   1/1     Running   0          68s
    pod-to-external-fqdn-allow-google-cnp-b7b6bcdcb-97p75   1/1     Running   0          68s
 
diff --git a/pkg/endpoint/endpoint_test.go b/pkg/endpoint/endpoint_test.go
index 8591dafbc2..49507c9586 100644
--- a/pkg/endpoint/endpoint_test.go
+++ b/pkg/endpoint/endpoint_test.go
@@ -506,8 +506,8 @@ func (s *EndpointSuite) TestWaitForPolicyRevision(c *C) {
 func (s *EndpointSuite) TestProxyID(c *C) {
 	e := &Endpoint{ID: 123, policyRevision: 0}
 
-	id, err := e.ProxyID(nil, &policy.L4Filter{Port: 8080, Protocol: api.ProtoTCP, Ingress: true})
-	c.Assert(err, IsNil)
+	id := e.proxyID(&policy.L4Filter{Port: 8080, Protocol: api.ProtoTCP, Ingress: true})
+	c.Assert(id, Not(Equals), "")
 	endpointID, ingress, protocol, port, err := policy.ParseProxyID(id)
 	c.Assert(endpointID, Equals, uint16(123))
 	c.Assert(ingress, Equals, true)
diff --git a/pkg/endpoint/redirect_test.go b/pkg/endpoint/redirect_test.go
index efb4c3e920..1a7669a8af 100644
--- a/pkg/endpoint/redirect_test.go
+++ b/pkg/endpoint/redirect_test.go
@@ -63,7 +63,7 @@ func (r *RedirectSuiteProxy) RemoveRedirect(id string, wg *completion.WaitGroup)
 }
 
 // UpdateNetworkPolicy does nothing.
-func (r *RedirectSuiteProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
+func (r *RedirectSuiteProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
 	return nil, nil
 }
 
diff --git a/pkg/envoy/server_test.go b/pkg/envoy/server_test.go
index 47bef9928f..68173342ca 100644
--- a/pkg/envoy/server_test.go
+++ b/pkg/envoy/server_test.go
@@ -24,6 +24,8 @@ import (
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/policy/api"
 	"github.com/cilium/cilium/pkg/policy/api/kafka"
+	"github.com/cilium/cilium/pkg/proxy/logger"
+	"github.com/cilium/cilium/pkg/proxy/logger/test"
 
 	"github.com/cilium/proxy/go/cilium/api"
 	envoy_config_core "github.com/cilium/proxy/go/envoy/config/core/v3"
@@ -44,6 +46,14 @@ var (
 	_        = Suite(&ServerSuite{})
 	IPv4Addr = "10.1.1.1"
 	Identity = identity.NumericIdentity(123)
+
+	ep logger.EndpointUpdater = &test.ProxyUpdaterMock{
+		Id:       1000,
+		Ipv4:     "10.0.0.1",
+		Ipv6:     "f00d::1",
+		Labels:   []string{"id.foo", "id.bar"},
+		Identity: Identity,
+	}
 )
 
 var PortRuleHTTP1 = &api.PortRuleHTTP{
@@ -486,99 +496,106 @@ func (s *ServerSuite) TestGetPortNetworkPolicyRule(c *C) {
 
 func (s *ServerSuite) TestGetDirectionNetworkPolicy(c *C) {
 	// L4+L7
-	obtained := getDirectionNetworkPolicy(L4PolicyMap1, nil, true)
+	obtained := getDirectionNetworkPolicy(ep, L4PolicyMap1, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies1)
 
 	// L4+L7 with header mods
-	obtained = getDirectionNetworkPolicy(L4PolicyMap1HeaderMatch, nil, true)
+	obtained = getDirectionNetworkPolicy(ep, L4PolicyMap1HeaderMatch, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies1HeaderMatch)
 
 	// L4+L7
-	obtained = getDirectionNetworkPolicy(L4PolicyMap2, nil, true)
+	obtained = getDirectionNetworkPolicy(ep, L4PolicyMap2, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies2)
 
 	// L4-only
-	obtained = getDirectionNetworkPolicy(L4PolicyMap4, nil, true)
+	obtained = getDirectionNetworkPolicy(ep, L4PolicyMap4, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies6)
 
 	// L4-only
-	obtained = getDirectionNetworkPolicy(L4PolicyMap5, nil, true)
+	obtained = getDirectionNetworkPolicy(ep, L4PolicyMap5, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies7)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicy(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy1, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4Policy1, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPolicies1,
 		EgressPerPortPolicies:  ExpectedPerPortPolicies2,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyWildcard(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4Policy2, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPolicies3,
 		EgressPerPortPolicies:  ExpectedPerPortPolicies2,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyDeny(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy1RequiresV2, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4Policy1RequiresV2, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPolicies4RequiresV2,
 		EgressPerPortPolicies:  ExpectedPerPortPolicies2,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyWildcardDeny(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2RequiresV2, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4Policy2RequiresV2, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPolicies5RequiresV2,
 		EgressPerPortPolicies:  ExpectedPerPortPolicies2,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyNil(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", nil, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: nil,
 		EgressPerPortPolicies:  nil,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyIngressNotEnforced(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2, nil, false, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4Policy2, false, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: allowAllPortNetworkPolicy,
 		EgressPerPortPolicies:  ExpectedPerPortPolicies2,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyEgressNotEnforced(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2RequiresV2, nil, true, false)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4Policy2RequiresV2, true, false)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPolicies5RequiresV2,
 		EgressPerPortPolicies:  allowAllPortNetworkPolicy,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
@@ -630,11 +647,12 @@ var ExpectedPerPortPoliciesL7 = []*cilium.PortNetworkPolicy{
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyL7(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4PolicyL7, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4PolicyL7, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPoliciesL7,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
@@ -684,11 +702,12 @@ var ExpectedPerPortPoliciesKafka = []*cilium.PortNetworkPolicy{
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyKafka(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4PolicyKafka, nil, true, true)
+	obtained := getNetworkPolicy(ep, IPv4Addr, L4PolicyKafka, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
 		IngressPerPortPolicies: ExpectedPerPortPoliciesKafka,
+		ConntrackMapName:       "global",
 	}
 	c.Assert(obtained, checker.Equals, expected)
 }
diff --git a/pkg/ipcache/ipcache_test.go b/pkg/ipcache/ipcache_test.go
index 3c280c4146..f9e6ab34cd 100644
--- a/pkg/ipcache/ipcache_test.go
+++ b/pkg/ipcache/ipcache_test.go
@@ -277,9 +277,9 @@ func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
 	meta := K8sMetadata{
 		Namespace: "default",
 		PodName:   "app1",
-		NamedPorts: policy.NamedPortsMap{
-			"http": policy.NamedPort{Port: 80, Proto: uint8(u8proto.TCP)},
-			"dns":  policy.NamedPort{Port: 53},
+		NamedPorts: policy.NamedPortMap{
+			"http": policy.PortProto{Port: 80, Proto: uint8(u8proto.TCP)},
+			"dns":  policy.PortProto{Port: 53},
 		},
 	}
 
@@ -301,14 +301,12 @@ func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
 	c.Assert(cachedIdentity.Source, Equals, source.Kubernetes)
 
 	// Named ports have been updated
-	c.Assert(namedPortsChanged, Equals, true)
+	c.Assert(namedPortsChanged, Equals, false) // not before GetNamedPorts() has been called once
 	npm := IPIdentityCache.GetNamedPorts()
 	c.Assert(npm, NotNil)
 	c.Assert(len(npm), Equals, 2)
-	c.Assert(npm["http"].Port, Equals, uint16(80))
-	c.Assert(npm["http"].Proto, Equals, uint8(6)) // TCP
-	c.Assert(npm["dns"].Port, Equals, uint16(53))
-	c.Assert(npm["dns"].Proto, Equals, uint8(0)) // ANY
+	c.Assert(npm["http"], checker.HasKey, policy.PortProto{Port: uint16(80), Proto: uint8(6)})
+	c.Assert(npm["dns"], checker.HasKey, policy.PortProto{Port: uint16(53), Proto: uint8(0)})
 
 	// No duplicates.
 	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
@@ -328,9 +326,9 @@ func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
 	meta2 := K8sMetadata{
 		Namespace: "testing",
 		PodName:   "app2",
-		NamedPorts: policy.NamedPortsMap{
-			"https": policy.NamedPort{Port: 443, Proto: uint8(u8proto.TCP)},
-			"dns":   policy.NamedPort{Port: 53},
+		NamedPorts: policy.NamedPortMap{
+			"https": policy.PortProto{Port: 443, Proto: uint8(u8proto.TCP)},
+			"dns":   policy.PortProto{Port: 53},
 		},
 	}
 
@@ -356,22 +354,17 @@ func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
 	npm = IPIdentityCache.GetNamedPorts()
 	c.Assert(npm, NotNil)
 	c.Assert(len(npm), Equals, 3)
-	c.Assert(npm["http"].Port, Equals, uint16(80))
-	c.Assert(npm["http"].Proto, Equals, uint8(6)) // TCP
-	c.Assert(npm["dns"].Port, Equals, uint16(53))
-	c.Assert(npm["dns"].Proto, Equals, uint8(0)) // ANY
-	c.Assert(npm["https"].Port, Equals, uint16(443))
-	c.Assert(npm["https"].Proto, Equals, uint8(6)) // TCP
+	c.Assert(npm["http"], checker.HasKey, policy.PortProto{Port: uint16(80), Proto: uint8(6)})
+	c.Assert(npm["dns"], checker.HasKey, policy.PortProto{Port: uint16(53), Proto: uint8(0)})
+	c.Assert(npm["https"], checker.HasKey, policy.PortProto{Port: uint16(443), Proto: uint8(6)})
 
 	namedPortsChanged = IPIdentityCache.Delete(endpointIP, source.Kubernetes)
 	c.Assert(namedPortsChanged, Equals, true)
 	npm = IPIdentityCache.GetNamedPorts()
 	c.Assert(npm, NotNil)
 	c.Assert(len(npm), Equals, 2)
-	c.Assert(npm["dns"].Port, Equals, uint16(53))
-	c.Assert(npm["dns"].Proto, Equals, uint8(0)) // ANY
-	c.Assert(npm["https"].Port, Equals, uint16(443))
-	c.Assert(npm["https"].Proto, Equals, uint8(6)) // TCP
+	c.Assert(npm["dns"], checker.HasKey, policy.PortProto{Port: uint16(53), Proto: uint8(0)})
+	c.Assert(npm["https"], checker.HasKey, policy.PortProto{Port: uint16(443), Proto: uint8(6)})
 
 	// Assure deletion occurs across all mappings.
 	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
@@ -444,8 +437,8 @@ func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
 	// Test mapping of multiple IPs to same identity.
 	endpointIPs := []string{"192.168.0.1", "20.3.75.3", "27.2.2.2", "127.0.0.1", "127.0.0.1"}
 	identities := []identityPkg.NumericIdentity{5, 67, 29, 29, 29}
-	k8sMeta.NamedPorts = policy.NamedPortsMap{
-		"http2": policy.NamedPort{Port: 8080, Proto: uint8(u8proto.TCP)},
+	k8sMeta.NamedPorts = policy.NamedPortMap{
+		"http2": policy.PortProto{Port: 8080, Proto: uint8(u8proto.TCP)},
 	}
 
 	for index := range endpointIPs {
@@ -457,7 +450,7 @@ func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
 		c.Assert(updated, Equals, true)
 		npm = IPIdentityCache.GetNamedPorts()
 		c.Assert(npm, NotNil)
-		c.Assert(npm["http2"].Port, Equals, uint16(8080))
+		c.Assert(npm["http2"], checker.HasKey, policy.PortProto{Port: uint16(8080), Proto: uint8(6)})
 		// only the first changes named ports, as they are all the same
 		c.Assert(namedPortsChanged, Equals, index == 0)
 		cachedIdentity, _ := IPIdentityCache.LookupByIP(endpointIPs[index])
diff --git a/pkg/logging/limiter_test.go b/pkg/logging/limiter_test.go
new file mode 100644
index 0000000000..af9b2239ff
--- /dev/null
+++ b/pkg/logging/limiter_test.go
@@ -0,0 +1,59 @@
+// Copyright 2020 Authors of Cilium
+//
+// 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.
+
+// +build !privileged_tests
+
+package logging
+
+import (
+	"time"
+
+	. "gopkg.in/check.v1"
+)
+
+func (s *LoggingSuite) TestLimiter(c *C) {
+	// Set up a limiter that allows one event every half second with the burts of 3.
+	// The underlying token bucket has the capacity of three and fill rate of
+	// 2 per second.
+	limiter := NewLimiter(500*time.Millisecond, 3)
+
+	// Initially tree events should be allowed and the rest denied.
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, false)
+	c.Assert(limiter.Allow(), Equals, false)
+	c.Assert(limiter.Allow(), Equals, false)
+
+	// After half second one more event should be allowed, the rest denied
+	time.Sleep(500 * time.Millisecond)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, false)
+	c.Assert(limiter.Allow(), Equals, false)
+
+	// After one more second two events should be allowed, the rest denied
+	time.Sleep(1 * time.Second)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, false)
+	c.Assert(limiter.Allow(), Equals, false)
+
+	// After two more seconds three events should be allowed, the rest denied
+	time.Sleep(2 * time.Second)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, true)
+	c.Assert(limiter.Allow(), Equals, false)
+	c.Assert(limiter.Allow(), Equals, false)
+}
diff --git a/pkg/policy/distillery_test.go b/pkg/policy/distillery_test.go
index 72c4596268..2d977eadd9 100644
--- a/pkg/policy/distillery_test.go
+++ b/pkg/policy/distillery_test.go
@@ -311,7 +311,7 @@ func (d *policyDistillery) WithLogBuffer(w io.Writer) *policyDistillery {
 
 // distillPolicy distills the policy repository into a set of bpf map state
 // entries for an endpoint with the specified labels.
-func (d *policyDistillery) distillPolicy(npMap NamedPortsMap, epLabels labels.LabelArray) (MapState, error) {
+func (d *policyDistillery) distillPolicy(owner PolicyOwner, epLabels labels.LabelArray) (MapState, error) {
 	result := make(MapState)
 
 	endpointSelected, _ := d.Repository.GetRulesMatching(epLabels)
@@ -339,7 +339,7 @@ func (d *policyDistillery) distillPolicy(npMap NamedPortsMap, epLabels labels.La
 	io.WriteString(d.log, "[distill] Producing L4 filter keys\n")
 	for _, l4 := range l4IngressPolicy {
 		io.WriteString(d.log, fmt.Sprintf("[distill] Processing L4Filter (l4: %d/%s), (l3/7: %+v)\n", l4.Port, l4.Protocol, l4.L7RulesPerSelector))
-		for key, entry := range l4.ToMapState(npMap, 0) {
+		for key, entry := range l4.ToMapState(owner, 0) {
 			io.WriteString(d.log, fmt.Sprintf("[distill] L4 ingress allow %+v (parser=%s, redirect=%t)\n", key, l4.L7Parser, entry.IsRedirectEntry()))
 			result[key] = entry
 		}
@@ -375,7 +375,7 @@ func Test_MergeL3(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(nil, labelsFoo)
+			mapstate, err := repo.distillPolicy(DummyOwner{}, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
@@ -447,7 +447,7 @@ func Test_MergeRules(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(nil, labelsFoo)
+			mapstate, err := repo.distillPolicy(DummyOwner{}, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
@@ -466,10 +466,6 @@ func Test_MergeRulesWithNamedPorts(t *testing.T) {
 	}
 	selectorCache := testNewSelectorCache(identityCache)
 
-	npMap := NamedPortsMap{
-		"port-80": NamedPort{Proto: uint8(0), Port: uint16(80)},
-	}
-
 	tests := []struct {
 		test   int
 		rules  api.Rules
@@ -523,7 +519,7 @@ func Test_MergeRulesWithNamedPorts(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(npMap, labelsFoo)
+			mapstate, err := repo.distillPolicy(DummyOwner{}, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
@@ -564,7 +560,7 @@ func Test_AllowAll(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(nil, labelsFoo)
+			mapstate, err := repo.distillPolicy(DummyOwner{}, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
diff --git a/pkg/policy/portmap_test.go b/pkg/policy/portmap_test.go
new file mode 100644
index 0000000000..d5091772a0
--- /dev/null
+++ b/pkg/policy/portmap_test.go
@@ -0,0 +1,196 @@
+// Copyright 2019 Authors of Cilium
+//
+// 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.
+
+// +build !privileged_tests
+
+package policy
+
+import (
+	"github.com/cilium/cilium/pkg/u8proto"
+
+	. "gopkg.in/check.v1"
+)
+
+func (ds *PolicyTestSuite) TestPolicyValidateName(c *C) {
+	name, err := ValidatePortName("Http")
+	c.Assert(err, IsNil)
+	c.Assert(name, Equals, "http")
+
+	name, err = ValidatePortName("dns-tcp")
+	c.Assert(err, IsNil)
+	c.Assert(name, Equals, "dns-tcp")
+
+	_, err = ValidatePortName("-http")
+	c.Assert(err, Not(IsNil))
+
+	_, err = ValidatePortName("http-")
+	c.Assert(err, Not(IsNil))
+
+	name, err = ValidatePortName("http-80")
+	c.Assert(err, IsNil)
+	c.Assert(name, Equals, "http-80")
+
+	_, err = ValidatePortName("http--s")
+	c.Assert(err, Not(IsNil))
+}
+
+func (ds *PolicyTestSuite) TestPolicyNewPortProto(c *C) {
+	np, err := newPortProto(80, "tcp")
+	c.Assert(err, IsNil)
+	c.Assert(np, Equals, PortProto{Port: uint16(80), Proto: uint8(6)})
+
+	_, err = newPortProto(88888, "tcp")
+	c.Assert(err, Not(IsNil))
+	c.Assert(err.Error(), Equals, "Port number 88888 out of 16-bit range")
+
+	_, err = newPortProto(80, "cccp")
+	c.Assert(err, Not(IsNil))
+	c.Assert(err.Error(), Equals, "unknown protocol 'cccp'")
+
+	np, err = newPortProto(88, "")
+	c.Assert(err, IsNil)
+	c.Assert(np, Equals, PortProto{Port: uint16(88), Proto: uint8(6)})
+}
+
+func (ds *PolicyTestSuite) TestPolicyNamedPortMap(c *C) {
+	npm := make(NamedPortMap)
+
+	err := npm.AddPort("http", 80, "tcp")
+	c.Assert(err, IsNil)
+	c.Assert(npm, HasLen, 1)
+
+	err = npm.AddPort("dns", 53, "UDP")
+	c.Assert(err, IsNil)
+	c.Assert(npm, HasLen, 2)
+
+	err = npm.AddPort("zero", 0, "TCP")
+	c.Assert(err, Equals, ErrNamedPortIsZero)
+	c.Assert(npm, HasLen, 2)
+
+	proto, err := u8proto.ParseProtocol("UDP")
+	c.Assert(err, IsNil)
+	c.Assert(uint8(proto), Equals, uint8(17))
+
+	port, err := npm.GetNamedPort("dns", uint8(proto))
+	c.Assert(err, IsNil)
+	c.Assert(port, Equals, uint16(53))
+
+	port, err = npm.GetNamedPort("dns", uint8(6))
+	c.Assert(err, Equals, ErrIncompatibleProtocol)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = npm.GetNamedPort("unknown", uint8(proto))
+	c.Assert(err, Equals, ErrUnknownNamedPort)
+	c.Assert(port, Equals, uint16(0))
+}
+
+func (ds *PolicyTestSuite) TestPolicyPortProtoSet(c *C) {
+	a := PortProtoSet{
+		PortProto{Port: 80, Proto: 6}:  struct{}{},
+		PortProto{Port: 443, Proto: 6}: struct{}{},
+		PortProto{Port: 53, Proto: 17}: struct{}{},
+	}
+	b := PortProtoSet{
+		PortProto{Port: 80, Proto: 6}:  struct{}{},
+		PortProto{Port: 443, Proto: 6}: struct{}{},
+		PortProto{Port: 53, Proto: 6}:  struct{}{},
+	}
+	c.Assert(a.Equal(a), Equals, true)
+	c.Assert(a.Equal(b), Equals, false)
+	c.Assert(b.Equal(b), Equals, true)
+}
+
+func (ds *PolicyTestSuite) TestPolicyNamedPortMultiMap(c *C) {
+	a := NamedPortMultiMap{
+		"http": PortProtoSet{
+			PortProto{Port: 80, Proto: 6}:   struct{}{},
+			PortProto{Port: 8080, Proto: 6}: struct{}{},
+		},
+		"https": PortProtoSet{
+			PortProto{Port: 443, Proto: 6}: struct{}{},
+		},
+		"zero": PortProtoSet{
+			PortProto{Port: 0, Proto: 6}: struct{}{},
+		},
+		"none": PortProtoSet{},
+		"dns": PortProtoSet{
+			PortProto{Port: 53, Proto: 17}: struct{}{},
+			PortProto{Port: 53, Proto: 6}:  struct{}{},
+		},
+	}
+	b := NamedPortMultiMap{
+		"http": PortProtoSet{
+			PortProto{Port: 80, Proto: 6}:   struct{}{},
+			PortProto{Port: 8080, Proto: 6}: struct{}{},
+		},
+		"https": PortProtoSet{
+			PortProto{Port: 443, Proto: 6}: struct{}{},
+		},
+		"zero": PortProtoSet{
+			PortProto{Port: 0, Proto: 6}: struct{}{},
+		},
+		"none": PortProtoSet{},
+		"dns": PortProtoSet{
+			PortProto{Port: 53, Proto: 0}: struct{}{},
+		},
+	}
+
+	c.Assert(a.Equal(a), Equals, true)
+	c.Assert(a.Equal(b), Equals, false)
+	c.Assert(b.Equal(b), Equals, true)
+
+	port, err := a.GetNamedPort("http", 6)
+	c.Assert(err, Equals, ErrDuplicateNamedPorts)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = a.GetNamedPort("http", 17)
+	c.Assert(err, Equals, ErrIncompatibleProtocol)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = a.GetNamedPort("zero", 6)
+	c.Assert(err, Equals, ErrNamedPortIsZero)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = a.GetNamedPort("none", 6)
+	c.Assert(err, Equals, ErrUnknownNamedPort)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = a.GetNamedPort("unknown", 6)
+	c.Assert(err, Equals, ErrUnknownNamedPort)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = NamedPortMultiMap(nil).GetNamedPort("unknown", 6)
+	c.Assert(err, Equals, ErrNilMap)
+	c.Assert(port, Equals, uint16(0))
+
+	port, err = a.GetNamedPort("https", 6)
+	c.Assert(err, IsNil)
+	c.Assert(port, Equals, uint16(443))
+
+	port, err = a.GetNamedPort("dns", 6)
+	c.Assert(err, IsNil)
+	c.Assert(port, Equals, uint16(53))
+
+	port, err = b.GetNamedPort("dns", 6)
+	c.Assert(err, IsNil)
+	c.Assert(port, Equals, uint16(53))
+
+	port, err = a.GetNamedPort("dns", 17)
+	c.Assert(err, IsNil)
+	c.Assert(port, Equals, uint16(53))
+
+	port, err = b.GetNamedPort("dns", 17)
+	c.Assert(err, IsNil)
+	c.Assert(port, Equals, uint16(53))
+}
diff --git a/pkg/policy/resolve_test.go b/pkg/policy/resolve_test.go
index b5f7dfdf9c..8554c96152 100644
--- a/pkg/policy/resolve_test.go
+++ b/pkg/policy/resolve_test.go
@@ -174,12 +174,16 @@ func GenerateCIDRRules(numRules int) api.Rules {
 
 type DummyOwner struct{}
 
-func (d DummyOwner) LookupRedirectPortLocked(npMap NamedPortsMap, l4 *L4Filter) uint16 {
-	// Return a fake non-0 listening port number for redirect filters.
-	if l4.IsRedirect() {
-		return 4242
-	}
-	return 0
+func (d DummyOwner) LookupRedirectPortLocked(bool, string, uint16) uint16 {
+	return 4242
+}
+
+func (d DummyOwner) GetNamedPort(ingress bool, name string, proto uint8) uint16 {
+	return 80
+}
+
+func (d DummyOwner) GetNamedPortLocked(ingress bool, name string, proto uint8) uint16 {
+	return 80
 }
 
 func (d DummyOwner) GetID() uint64 {
@@ -220,7 +224,7 @@ func (ds *PolicyTestSuite) BenchmarkRegenerateCIDRPolicyRules(c *C) {
 	c.ResetTimer()
 	for i := 0; i < c.N; i++ {
 		ip, _ := testRepo.resolvePolicyLocked(fooIdentity)
-		_ = ip.DistillPolicy(DummyOwner{}, nil, false)
+		_ = ip.DistillPolicy(DummyOwner{}, false)
 		ip.Detach()
 	}
 }
@@ -230,7 +234,7 @@ func (ds *PolicyTestSuite) BenchmarkRegenerateL3IngressPolicyRules(c *C) {
 	c.ResetTimer()
 	for i := 0; i < c.N; i++ {
 		ip, _ := testRepo.resolvePolicyLocked(fooIdentity)
-		_ = ip.DistillPolicy(DummyOwner{}, nil, false)
+		_ = ip.DistillPolicy(DummyOwner{}, false)
 		ip.Detach()
 	}
 }
@@ -240,7 +244,7 @@ func (ds *PolicyTestSuite) BenchmarkRegenerateL3EgressPolicyRules(c *C) {
 	c.ResetTimer()
 	for i := 0; i < c.N; i++ {
 		ip, _ := testRepo.resolvePolicyLocked(fooIdentity)
-		_ = ip.DistillPolicy(DummyOwner{}, nil, false)
+		_ = ip.DistillPolicy(DummyOwner{}, false)
 		ip.Detach()
 	}
 }
@@ -282,7 +286,7 @@ func (ds *PolicyTestSuite) TestL7WithIngressWildcard(c *C) {
 	defer repo.Mutex.RUnlock()
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{}, nil, false)
+	policy := selPolicy.DistillPolicy(DummyOwner{}, false)
 
 	expectedEndpointPolicy := EndpointPolicy{
 		selectorPolicy: &selectorPolicy{
@@ -371,7 +375,7 @@ func (ds *PolicyTestSuite) TestL7WithLocalHostWildcardd(c *C) {
 
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{}, nil, false)
+	policy := selPolicy.DistillPolicy(DummyOwner{}, false)
 
 	cachedSelectorHost := testSelectorCache.FindCachedIdentitySelector(api.ReservedEndpointSelectors[labels.IDNameHost])
 	c.Assert(cachedSelectorHost, Not(IsNil))
@@ -459,7 +463,7 @@ func (ds *PolicyTestSuite) TestMapStateWithIngressWildcard(c *C) {
 	defer repo.Mutex.RUnlock()
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{}, nil, false)
+	policy := selPolicy.DistillPolicy(DummyOwner{}, false)
 
 	rule1MapStateEntry := NewMapStateEntry(labels.LabelArrayList{ruleLabel}, false)
 	allowEgressMapStateEntry := NewMapStateEntry(labels.LabelArrayList{ruleLabelAllowAnyEgress}, false)
@@ -564,7 +568,7 @@ func (ds *PolicyTestSuite) TestMapStateWithIngress(c *C) {
 	defer repo.Mutex.RUnlock()
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{}, nil, false)
+	policy := selPolicy.DistillPolicy(DummyOwner{}, false)
 
 	// Add new identity to test accumulation of PolicyMapChanges
 	added1 := cache.IdentityCache{
diff --git a/pkg/proxy/logger/test/epinfo.go b/pkg/proxy/logger/test/epinfo.go
index 682d902078..e94a5feb6b 100644
--- a/pkg/proxy/logger/test/epinfo.go
+++ b/pkg/proxy/logger/test/epinfo.go
@@ -20,7 +20,6 @@ import (
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/labels"
 	"github.com/cilium/cilium/pkg/lock"
-	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/proxy/accesslog"
 )
 
@@ -41,14 +40,14 @@ func (m *ProxyUpdaterMock) GetProxyInfoByFields() (uint64, string, string, []str
 func (m *ProxyUpdaterMock) UnconditionalRLock() { m.RWMutex.RLock() }
 func (m *ProxyUpdaterMock) RUnlock()            { m.RWMutex.RUnlock() }
 
-func (m *ProxyUpdaterMock) GetID() uint64                               { return m.Id }
-func (m *ProxyUpdaterMock) GetIPv4Address() string                      { return m.Ipv4 }
-func (m *ProxyUpdaterMock) GetIPv6Address() string                      { return m.Ipv6 }
-func (m *ProxyUpdaterMock) GetLabels() []string                         { return m.Labels }
-func (m *ProxyUpdaterMock) GetIdentityLocked() identity.NumericIdentity { return m.Identity }
-func (m *ProxyUpdaterMock) ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error) {
-	return "", nil
-}
+func (m *ProxyUpdaterMock) GetID() uint64                                 { return m.Id }
+func (m *ProxyUpdaterMock) GetIPv4Address() string                        { return m.Ipv4 }
+func (m *ProxyUpdaterMock) GetIPv6Address() string                        { return m.Ipv6 }
+func (m *ProxyUpdaterMock) GetLabels() []string                           { return m.Labels }
+func (m *ProxyUpdaterMock) GetEgressPolicyEnabledLocked() bool            { return true }
+func (m *ProxyUpdaterMock) GetIngressPolicyEnabledLocked() bool           { return true }
+func (m *ProxyUpdaterMock) GetIdentityLocked() identity.NumericIdentity   { return m.Identity }
+func (m *ProxyUpdaterMock) GetNamedPortLocked(bool, string, uint8) uint16 { return 0 }
 func (m *ProxyUpdaterMock) GetLabelsSHA() string {
 	return labels.NewLabelsFromModel(m.Labels).SHA256Sum()
 }