Repository: cilium/cilium
Pull request #9893 (2020-01-16 23:46:18)

eni: Deep copy CiliumNode resource before storing it in the node
Deep copy the resource before storing it. This way we are
not dependent on caller not using the resource after this
call. This may help avoid panics seen in the unit tests.

Fixes: #9779 (hopefully)
Signed-off-by: Jarno Rajahalme <jarno@covalent.io>

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cilium/cilium/9893)
<!-- Reviewable:end -->


maintainer-s-little-helper[bot]: Release note label not set, please set the appropriate release note.
jrajahalme: test-me-please
coveralls: 
[![Coverage Status](https://coveralls.io/builds/28163793/badge)](https://coveralls.io/builds/28163793)

Coverage increased (+0.04%) to 45.946% when pulling **763d78b98429ae317a1a807ebc05bc67da94055f on pr/jrajahalme/eni-copy-resource** into **abcfef66b34d773c8713e9c4d725d073738b7b2b on master**.


Patch:
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index 58993c936f..6dd18e1b8f 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -130,6 +130,7 @@ cilium-agent [flags]
       --monitor-queue-size int                                Size of the event queue when reading monitor events
       --mtu int                                               Overwrite auto-detected MTU of underlying network
       --nat46-range string                                    IPv6 prefix to map IPv4 addresses to (default "0:0:0:0:0:FFFF::/96")
+      --node-port-mode string                                 BPF NodePort mode ("snat", "dsr") (default "snat")
       --node-port-range strings                               Set the min/max NodePort port range (default [30000,32767])
       --policy-queue-size int                                 size of queues for policy-related events (default 100)
       --pprof                                                 Enable serving the pprof debugging API
diff --git a/USERS.md b/USERS.md
index ea8131148e..1e19fad35b 100644
--- a/USERS.md
+++ b/USERS.md
@@ -60,6 +60,11 @@ Users (Alphabetically)
       U: ENI networking, L3/L4 policies, FQDN based policy, FQDN filtering
       Q: ungureanuvladvictor
 
+    * N: Rapyuta Robotics.
+      D: Rapyuta is using cilium as their main CNI plugin. (self hosted k8s)
+      U: CiliumNetworkPolicy, Hubble, Service Load Balancing.
+      Q: @Gowtham
+
     * N: Sportradar
       D: Sportradar is using Cilium as their main CNI plugin in AWS (using kops)
       U: L3/L4 policies, Hubble, BPF NodePort, CiliumClusterwideNetworkPolicy
diff --git a/bpf/Makefile b/bpf/Makefile
index fef8135ca2..de392e0ba9 100644
--- a/bpf/Makefile
+++ b/bpf/Makefile
@@ -94,9 +94,10 @@ bpf_overlay.o: bpf_overlay.ll
 	$(QUIET) ${LLC} ${LLC_FLAGS} -filetype=obj -o $@ $(patsubst %.o,%.ll,$@)
 
 NETDEV_OPTIONS = $(LB_OPTIONS) \
-	-DENABLE_IPV4:-DENABLE_IPV6:-DLB_L3:-DLB_L4:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_MASQUERADE
+	-DENABLE_IPV4:-DENABLE_IPV6:-DLB_L3:-DLB_L4:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_MASQUERADE \
+	-DENABLE_IPV4:-DENABLE_IPV6:-DLB_L3:-DLB_L4:-DENABLE_HOST_SERVICES_UDP:-DENABLE_HOST_SERVICES_TCP:-DENABLE_NODEPORT:-DENABLE_EXTERNAL_IP:-DENABLE_DSR
 
-MAX_NETDEV_OPTIONS = $(MAX_LB_OPTIONS) -DENABLE_MASQUERADE
+MAX_NETDEV_OPTIONS = $(MAX_LB_OPTIONS) -DENABLE_DSR
 
 bpf_netdev.ll: bpf_netdev.c $(LIB)
 	$(QUIET) set -e; \
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 22d36ea925..8ee4a029ca 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -220,7 +220,14 @@ ct_recreate6:
 			ep_tail_call(skb, CILIUM_CALL_IPV6_NODEPORT_REVNAT);
 			return DROP_MISSED_TAIL_CALL;
 		}
-#endif
+# ifdef ENABLE_DSR
+		if (ct_state.dsr) {
+			ret = xlate_dsr_v6(skb, tuple, l4_off);
+			if (ret != 0)
+				return ret;
+		}
+# endif /* ENABLE_DSR */
+#endif /* ENABLE_NODEPORT */
 		if (ct_state.rev_nat_index) {
 			ret = lb6_rev_nat(skb, l4_off, &csum_off,
 					  ct_state.rev_nat_index, tuple, 0);
@@ -554,6 +561,13 @@ ct_recreate4:
 			ep_tail_call(skb, CILIUM_CALL_IPV4_NODEPORT_REVNAT);
 			return DROP_MISSED_TAIL_CALL;
 		}
+# ifdef ENABLE_DSR
+		if (ct_state.dsr) {
+			ret = xlate_dsr_v4(skb, &tuple, l4_off);
+			if (ret != 0)
+				return ret;
+		}
+# endif /* ENABLE_DSR */
 #endif /* ENABLE_NODEPORT */
 
 		if (ct_state.rev_nat_index) {
@@ -860,6 +874,16 @@ ipv6_policy(struct __sk_buff *skb, int ifindex, __u32 src_label, __u8 *reason)
 		verdict = 0;
 
 	if (ret == CT_NEW) {
+#ifdef ENABLE_DSR
+		bool dsr = false;
+
+		ret = handle_dsr_v6(skb, &dsr);
+		if (ret != 0)
+			return ret;
+
+		ct_state_new.dsr = dsr;
+#endif /* ENABLE_DSR */
+
 		ct_state_new.orig_dport = tuple.dport;
 		ct_state_new.src_sec_id = src_label;
 		ct_state_new.node_port = ct_state.node_port;
@@ -1048,6 +1072,16 @@ ipv4_policy(struct __sk_buff *skb, int ifindex, __u32 src_label, __u8 *reason, _
 		verdict = 0;
 
 	if (ret == CT_NEW) {
+#ifdef ENABLE_DSR
+		bool dsr = false;
+
+		ret = handle_dsr_v4(skb, &dsr);
+		if (ret != 0)
+			return ret;
+
+		ct_state_new.dsr = dsr;
+#endif /* ENABLE_DSR */
+
 		ct_state_new.orig_dport = tuple.dport;
 		ct_state_new.src_sec_id = src_label;
 		ct_state_new.node_port = ct_state.node_port;
diff --git a/bpf/bpf_netdev.c b/bpf/bpf_netdev.c
index 0bd04cc335..d604a23b22 100644
--- a/bpf/bpf_netdev.c
+++ b/bpf/bpf_netdev.c
@@ -714,14 +714,14 @@ int to_netdev(struct __sk_buff *skb)
 	 * workaround.
 	 */
 	int ret = TC_ACT_OK;
-#if defined(ENABLE_NODEPORT) || defined(ENABLE_MASQUERADE)
-#ifdef ENABLE_NODEPORT
+#if (defined(ENABLE_NODEPORT) && !defined(ENABLE_DSR)) || defined(ENABLE_MASQUERADE)
+# if defined(ENABLE_NODEPORT) && !defined(ENABLE_DSR)
 	if ((skb->mark & MARK_MAGIC_SNAT_DONE) == MARK_MAGIC_SNAT_DONE)
 		return TC_ACT_OK;
 	ret = nodeport_nat_fwd(skb, false);
 	if (IS_ERR(ret))
 		return send_drop_notify_error(skb, 0, ret, TC_ACT_SHOT, METRIC_EGRESS);
-#else
+# else
 	__u16 proto;
 	if (!validate_ethertype(skb, &proto))
 		/* Pass unknown traffic to the stack */
@@ -730,8 +730,8 @@ int to_netdev(struct __sk_buff *skb)
 	ret = snat_process(skb, BPF_PKT_DIR);
 	if (!ret)
 		cilium_dbg_capture(skb, DBG_CAPTURE_SNAT_POST, skb->ifindex);
-#endif /* ENABLE_NODEPORT */
-#endif /* ENABLE_NODEPORT || ENABLE_MASQUERADE */
+# endif
+#endif
 	return ret;
 }
 
diff --git a/bpf/include/bpf/api.h b/bpf/include/bpf/api.h
index c14dec5792..c7e0f1e8a4 100644
--- a/bpf/include/bpf/api.h
+++ b/bpf/include/bpf/api.h
@@ -162,6 +162,8 @@ static int BPF_FUNC(skb_load_bytes, struct __sk_buff *skb, uint32_t off,
 		    void *to, uint32_t len);
 static int BPF_FUNC(skb_store_bytes, struct __sk_buff *skb, uint32_t off,
 		    const void *from, uint32_t len, uint32_t flags);
+static int BPF_FUNC(skb_adjust_room, struct __sk_buff *skb, int32_t len_diff,
+		    uint32_t mode, uint64_t flags);
 
 static int BPF_FUNC(l3_csum_replace, struct __sk_buff *skb, uint32_t off,
 		    uint32_t from, uint32_t to, uint32_t flags);
diff --git a/bpf/lib/common.h b/bpf/lib/common.h
index 958f8e8b5d..eb8493f24d 100644
--- a/bpf/lib/common.h
+++ b/bpf/lib/common.h
@@ -89,7 +89,9 @@
 #define CILIUM_CALL_IPV4_NODEPORT_REVNAT	17
 #define CILIUM_CALL_IPV6_NODEPORT_REVNAT	18
 #define CILIUM_CALL_ENCAP_NODEPORT_NAT		19
-#define CILIUM_CALL_SIZE			20
+#define CILIUM_CALL_IPV4_NODEPORT_DSR		20
+#define CILIUM_CALL_IPV6_NODEPORT_DSR		21
+#define CILIUM_CALL_SIZE			22
 
 typedef __u64 mac_t;
 
@@ -382,6 +384,32 @@ enum {
 
 #define MARK_MAGIC_SNAT_DONE		0x0500
 
+/* IPv4 option used to carry service addr and port for DSR. Lower 16bits set to
+ * zero so that they can be OR'd with service port.
+ *
+ * Copy = 1 (option is copied to each fragment)
+ * Class = 0 (control option)
+ * Number = 26 (not used according to [1])
+ * Len = 8 (option type (1) + option len (1) + addr (4) + port (2))
+ *
+ * [1]: https://www.iana.org/assignments/ip-parameters/ip-parameters.xhtml
+ * */
+#define DSR_IPV4_OPT_32		0x9a080000
+#define DSR_IPV4_OPT_MASK	0xffff0000
+#define DSR_IPV4_DPORT_MASK	0x0000ffff
+
+/* IPv6 option type of Destination Option used to carry service IPv6 addr and
+ * port for DSR.
+ *
+ * 0b00		- "skip over this option and continue processing the header"
+ *     0	- "Option Data does not change en-route"
+ *      11011   - Unassigned [1]
+ *
+ * [1]:  https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml#ipv6-parameters-2
+ */
+#define DSR_IPV6_OPT_TYPE	0x1B
+#define DSR_IPV6_OPT_LEN	0x14	// to store ipv6 addr + port
+#define DSR_IPV6_EXT_LEN	0x2	// = (sizeof(dsr_opt_v6) - 8) / 8
 /**
  * get_identity - returns source identity from the mark field
  */
@@ -457,11 +485,17 @@ static inline void __inline__ set_encrypt_key_cb(struct __sk_buff *skb, __u8 key
 /* skb->cb[] usage: */
 enum {
 	CB_SRC_LABEL,
+#define	CB_SVC_PORT		CB_SRC_LABEL	/* Alias, non-overlapping */
 	CB_IFINDEX,
+#define	CB_SVC_ADDR_V4		CB_IFINDEX	/* Alias, non-overlapping */
+#define	CB_SVC_ADDR_V6_1	CB_IFINDEX	/* Alias, non-overlapping */
 	CB_POLICY,
+#define	CB_SVC_ADDR_V6_2	CB_POLICY	/* Alias, non-overlapping */
 	CB_NAT46_STATE,
-#define CB_NAT		CB_NAT46_STATE	/* Alias, non-overlapping */
+#define CB_NAT			CB_NAT46_STATE	/* Alias, non-overlapping */
+#define	CB_SVC_ADDR_V6_3	CB_NAT46_STATE	/* Alias, non-overlapping */
 	CB_CT_STATE,
+#define	CB_SVC_ADDR_V6_4	CB_CT_STATE	/* Alias, non-overlapping */
 };
 
 /* State values for NAT46 */
@@ -532,7 +566,8 @@ struct ct_entry {
 	      seen_non_syn:1,
 	      node_port:1,
 	      proxy_redirect:1, // Connection is redirected to a proxy
-	      reserved:9;
+	      dsr:1,
+	      reserved:8;
 	__u16 rev_nat_index;
 	__u16 backend_id; /* Populated only in v1.6+ BPF code. */
 
@@ -619,7 +654,8 @@ struct ct_state {
 	__u16 loopback:1,
 	      node_port:1,
 	      proxy_redirect:1, // Connection is redirected to a proxy
-	      reserved:13;
+	      dsr:1,
+	      reserved:12;
 	__be16 orig_dport;
 	__be32 addr;
 	__be32 svc_addr;
diff --git a/bpf/lib/conntrack.h b/bpf/lib/conntrack.h
index 8a1ae8121c..df1c926bfc 100644
--- a/bpf/lib/conntrack.h
+++ b/bpf/lib/conntrack.h
@@ -230,6 +230,7 @@ static inline __u8 __inline__ __ct_lookup(void *map, struct __sk_buff *skb,
 			ct_state->rev_nat_index = entry->rev_nat_index;
 			ct_state->loopback = entry->lb_loopback;
 			ct_state->node_port = entry->node_port;
+			ct_state->dsr = entry->dsr;
 			ct_state->proxy_redirect = entry->proxy_redirect;
 			/* To support seamless upgrade from an earlier service
 			 * implementation, we store references to the backend
@@ -666,6 +667,7 @@ static inline int __inline__ ct_create6(void *map, struct ipv6_ct_tuple *tuple,
 
 	entry.lb_loopback = ct_state->loopback;
 	entry.node_port = ct_state->node_port;
+	entry.dsr = ct_state->dsr;
 
 	entry.rev_nat_index = ct_state->rev_nat_index;
 	seen_flags.value |= is_tcp ? TCP_FLAG_SYN : 0;
@@ -753,6 +755,7 @@ static inline int __inline__ ct_create4(void *map, struct ipv4_ct_tuple *tuple,
 	
 	entry.lb_loopback = ct_state->loopback;
 	entry.node_port = ct_state->node_port;
+	entry.dsr = ct_state->dsr;
 
 	/* We need to store the backend_id (points to a svc v2 endpoint), while
 	 * handling migration for users upgrading from prior releases. where
diff --git a/bpf/lib/nat.h b/bpf/lib/nat.h
index e6fe68ed01..42afca5bd8 100644
--- a/bpf/lib/nat.h
+++ b/bpf/lib/nat.h
@@ -441,6 +441,54 @@ static __always_inline bool snat_v4_can_skip(const struct ipv4_nat_target *targe
 	return false;
 }
 
+static __always_inline int snat_v4_create_dsr(struct __sk_buff *skb,
+					      __be32 to_saddr, __be16 to_sport)
+{
+	void *data, *data_end;
+	struct ipv4_ct_tuple tuple = {};
+	struct ipv4_nat_entry state = {};
+	struct iphdr *ip4;
+	struct {
+		__be16 sport;
+		__be16 dport;
+	} l4hdr;
+	__u32 off;
+
+	build_bug_on(sizeof(struct ipv4_nat_entry) > 64);
+
+	if (!revalidate_data(skb, &data, &data_end, &ip4))
+		return DROP_INVALID;
+
+	tuple.nexthdr = ip4->protocol;
+	tuple.daddr = ip4->saddr;
+	tuple.saddr = ip4->daddr;
+	tuple.flags = NAT_DIR_EGRESS;
+	off = ((void *)ip4 - data) + ipv4_hdrlen(ip4);
+	switch (tuple.nexthdr) {
+	case IPPROTO_TCP:
+	case IPPROTO_UDP:
+		if (skb_load_bytes(skb, off, &l4hdr, sizeof(l4hdr)) < 0)
+			return DROP_INVALID;
+		tuple.dport = l4hdr.sport;
+		tuple.sport = l4hdr.dport;
+		break;
+	default:
+		// NodePort svc can be reached only via TCP or UDP, so
+		// drop the rest
+		return DROP_NAT_UNSUPP_PROTO;
+	}
+
+	state.common.created = bpf_ktime_get_nsec();
+	state.to_saddr = to_saddr;
+	state.to_sport = to_sport;
+
+	int ret = map_update_elem(&SNAT_MAPPING_IPV4, &tuple, &state, 0);
+	if (ret)
+		return ret;
+
+	return TC_ACT_OK;
+}
+
 static __always_inline int snat_v4_process(struct __sk_buff *skb, int dir,
 					   const struct ipv4_nat_target *target)
 {
@@ -845,6 +893,60 @@ static __always_inline bool snat_v6_can_skip(const struct ipv6_nat_target *targe
 	return false;
 }
 
+static __always_inline int snat_v6_create_dsr(struct __sk_buff *skb,
+					      union v6addr *to_saddr,
+					      __be16 to_sport)
+{
+	void *data, *data_end;
+	struct ipv6_ct_tuple tuple = {};
+	struct ipv6_nat_entry state = {};
+	struct ipv6hdr *ip6;
+	struct {
+		__be16 sport;
+		__be16 dport;
+	} l4hdr;
+	__u32 off;
+	int hdrlen;
+
+	build_bug_on(sizeof(struct ipv6_nat_entry) > 64);
+
+	if (!revalidate_data(skb, &data, &data_end, &ip6))
+		return DROP_INVALID;
+
+	tuple.nexthdr = ip6->nexthdr;
+	hdrlen = ipv6_hdrlen(skb, ETH_HLEN, &tuple.nexthdr);
+	if (hdrlen < 0)
+		return hdrlen;
+
+	ipv6_addr_copy(&tuple.daddr, (union v6addr *)&ip6->saddr);
+	ipv6_addr_copy(&tuple.saddr, (union v6addr *)&ip6->daddr);
+	tuple.flags = NAT_DIR_EGRESS;
+	off = ((void *)ip6 - data) + hdrlen;
+	switch (tuple.nexthdr) {
+	case IPPROTO_TCP:
+	case IPPROTO_UDP:
+		if (skb_load_bytes(skb, off, &l4hdr, sizeof(l4hdr)) < 0)
+			return DROP_INVALID;
+		tuple.dport = l4hdr.sport;
+		tuple.sport = l4hdr.dport;
+		break;
+	default:
+		// NodePort svc can be reached only via TCP or UDP, so
+		// drop the rest
+		return DROP_NAT_UNSUPP_PROTO;
+	}
+
+	state.common.created = bpf_ktime_get_nsec();
+	ipv6_addr_copy(&state.to_saddr, to_saddr);
+	state.to_sport = to_sport;
+
+	int ret = map_update_elem(&SNAT_MAPPING_IPV6, &tuple, &state, 0);
+	if (ret)
+		return ret;
+
+	return TC_ACT_OK;
+}
+
 static __always_inline int snat_v6_process(struct __sk_buff *skb, int dir,
 					   const struct ipv6_nat_target *target)
 {
diff --git a/bpf/lib/nodeport.h b/bpf/lib/nodeport.h
index ee9766e7d3..46a203194f 100644
--- a/bpf/lib/nodeport.h
+++ b/bpf/lib/nodeport.h
@@ -55,6 +55,17 @@ struct bpf_elf_map __section_maps NODEPORT_NEIGH6 = {
 	.pinning	= PIN_GLOBAL_NS,
 	.max_elem	= SNAT_MAPPING_IPV6_SIZE,
 };
+# ifdef ENABLE_DSR
+// The IPv6 extension should be 8-bytes aligned
+struct dsr_opt_v6 {
+	__u8 nexthdr;
+	__u8 len;
+	__u8 opt_type;
+	__u8 opt_len;
+	union v6addr addr;
+	__be32 port;
+};
+# endif
 #endif /* ENABLE_IPV6 */
 
 #endif /* ENABLE_NODEPORT */
@@ -119,6 +130,175 @@ static __always_inline int nodeport_nat_ipv6_rev(struct __sk_buff *skb,
 	return NODEPORT_DO_NAT_IPV6(addr, NAT_DIR_INGRESS);
 }
 
+# ifdef ENABLE_DSR
+
+static __always_inline int set_dsr_ext6(struct __sk_buff *skb,
+					struct ipv6hdr *ip6,
+					union v6addr *svc_addr, __be32 svc_port)
+{
+	struct dsr_opt_v6 opt = {};
+
+	opt.nexthdr = ip6->nexthdr;
+	ip6->nexthdr = NEXTHDR_DEST;
+	ip6->payload_len = bpf_htons(bpf_ntohs(ip6->payload_len) + 24);
+
+	opt.len = DSR_IPV6_EXT_LEN;
+	opt.opt_type = DSR_IPV6_OPT_TYPE;
+	opt.opt_len = DSR_IPV6_OPT_LEN;
+	ipv6_addr_copy(&opt.addr, svc_addr);
+	opt.port = svc_port;
+
+	if (skb_adjust_room(skb, sizeof(opt), BPF_ADJ_ROOM_NET, 0))
+		return DROP_INVALID;
+
+	if (skb_store_bytes(skb, ETH_HLEN + sizeof(*ip6), &opt, sizeof(opt), 0) < 0)
+		return DROP_INVALID;
+
+	return 0;
+}
+
+static __always_inline int find_dsr_v6(struct __sk_buff *skb, __u8 nexthdr,
+				       struct dsr_opt_v6 *dsr_opt, bool *found)
+{
+	int i, len = sizeof(struct ipv6hdr);
+	struct ipv6_opt_hdr opthdr;
+	__u8 nh = nexthdr;
+
+#pragma unroll
+	for (i = 0; i < IPV6_MAX_HEADERS; i++) {
+		switch (nh) {
+		case NEXTHDR_NONE:
+			return DROP_INVALID_EXTHDR;
+
+		case NEXTHDR_FRAGMENT:
+			return DROP_FRAG_NOSUPPORT;
+
+		case NEXTHDR_HOP:
+		case NEXTHDR_ROUTING:
+		case NEXTHDR_AUTH:
+		case NEXTHDR_DEST:
+			if (skb_load_bytes(skb, ETH_HLEN + len, &opthdr, sizeof(opthdr)) < 0)
+				return DROP_INVALID;
+
+			if (nh == NEXTHDR_DEST && opthdr.hdrlen == DSR_IPV6_EXT_LEN) {
+				if (skb_load_bytes(skb, ETH_HLEN + len, dsr_opt,
+						   sizeof(*dsr_opt)) < 0)
+					return DROP_INVALID;
+				if (dsr_opt->opt_type == DSR_IPV6_OPT_TYPE &&
+				    dsr_opt->opt_len == DSR_IPV6_OPT_LEN) {
+					*found = true;
+					return 0;
+				}
+			}
+
+			nh = opthdr.nexthdr;
+			if (nh == NEXTHDR_AUTH)
+				len += ipv6_authlen(&opthdr);
+			else
+				len += ipv6_optlen(&opthdr);
+			break;
+
+		default:
+			return len;
+		}
+	}
+
+	/* Reached limit of supported extension headers */
+	return DROP_INVALID_EXTHDR;
+}
+
+static __always_inline int handle_dsr_v6(struct __sk_buff *skb, bool *dsr)
+{
+	void *data, *data_end;
+	struct ipv6hdr *ip6;
+	struct dsr_opt_v6 opt = {};
+	int ret;
+
+	if (!revalidate_data(skb, &data, &data_end, &ip6))
+		return DROP_INVALID;
+
+	ret = find_dsr_v6(skb, ip6->nexthdr, &opt, dsr);
+	if (ret != 0)
+		return ret;
+
+	if (*dsr) {
+		if (snat_v6_create_dsr(skb, &opt.addr, opt.port) < 0) {
+			return DROP_INVALID;
+		}
+	}
+
+	return 0;
+}
+
+static __always_inline int xlate_dsr_v6(struct __sk_buff *skb,
+					struct ipv6_ct_tuple *tuple,
+					int l4_off)
+{
+	int ret;
+	struct ipv6_ct_tuple nat_tup = *tuple;
+	struct ipv6_nat_entry *entry;
+
+	nat_tup.flags = NAT_DIR_EGRESS;
+	nat_tup.sport = tuple->dport;
+	nat_tup.dport = tuple->sport;
+	entry = snat_v6_lookup(&nat_tup);
+	if (entry) {
+		ret = snat_v6_rewrite_egress(skb, &nat_tup, entry, l4_off);
+		if (ret != 0) {
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+__section_tail(CILIUM_MAP_CALLS, CILIUM_CALL_IPV6_NODEPORT_DSR)
+int tail_nodeport_ipv6_dsr(struct __sk_buff *skb)
+{
+	void *data, *data_end;
+	struct ipv6hdr *ip6;
+	union v6addr addr = {};
+	struct bpf_fib_lookup fib_params = {};
+	int ret;
+
+	if (!revalidate_data(skb, &data, &data_end, &ip6))
+		return DROP_INVALID;
+
+	__be32 dport = skb->cb[CB_SVC_PORT];
+	addr.p1 = skb->cb[CB_SVC_ADDR_V6_1];
+	addr.p2 = skb->cb[CB_SVC_ADDR_V6_2];
+	addr.p3 = skb->cb[CB_SVC_ADDR_V6_3];
+	addr.p4 = skb->cb[CB_SVC_ADDR_V6_4];
+	ret = set_dsr_ext6(skb, ip6, &addr, dport);
+	if (ret)
+		return ret;
+
+	if (!revalidate_data(skb, &data, &data_end, &ip6))
+		return DROP_INVALID;
+
+	fib_params.family = AF_INET6;
+	fib_params.ifindex = NATIVE_DEV_IFINDEX;
+	ipv6_addr_copy((union v6addr *) &fib_params.ipv6_src, (union v6addr *) &ip6->saddr);
+	ipv6_addr_copy((union v6addr *) &fib_params.ipv6_dst, (union v6addr *) &ip6->daddr);
+
+	ret = fib_lookup(skb, &fib_params, sizeof(fib_params),
+			 BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT);
+	if (ret != 0) {
+		return DROP_NO_FIB;
+	}
+
+	if (eth_store_daddr(skb, fib_params.dmac, 0) < 0) {
+		return DROP_WRITE_ERROR;
+	}
+	if (eth_store_saddr(skb, fib_params.smac, 0) < 0) {
+		return DROP_WRITE_ERROR;
+	}
+
+	return redirect(fib_params.ifindex, 0);
+}
+
+# endif /* ENABLE_DSR */
+
 __section_tail(CILIUM_MAP_CALLS, CILIUM_CALL_IPV6_NODEPORT_NAT)
 int tail_nodeport_nat_ipv6(struct __sk_buff *skb)
 {
@@ -236,7 +416,6 @@ static inline int nodeport_lb6(struct __sk_buff *skb, __u32 src_identity)
 	struct ct_state ct_state = {};
 	bool backend_local;
 	__u32 monitor = 0;
-	__u16 service_port;
 	union macaddr smac;
 
 	if (!revalidate_data(skb, &data, &data_end, &ip6))
@@ -270,11 +449,14 @@ static inline int nodeport_lb6(struct __sk_buff *skb, __u32 src_identity)
 	if (!svc || (!lb6_svc_is_external_ip(svc) && !lb6_svc_is_nodeport(svc))) {
 		if (svc)
 			return DROP_IS_CLUSTER_IP;
-
+#ifdef ENABLE_DSR
+		return TC_ACT_OK;
+#else
 		skb->cb[CB_NAT] = NAT_DIR_INGRESS;
 		skb->cb[CB_SRC_IDENTITY] = src_identity;
 		ep_tail_call(skb, CILIUM_CALL_IPV6_NODEPORT_NAT);
 		return DROP_MISSED_TAIL_CALL;
+#endif /* ENABLE_DSR */
 	}
 
 	ret = ct_lookup6(get_ct_map6(&tuple), &tuple, skb, l4_off, CT_EGRESS,
@@ -332,9 +514,19 @@ redo:
 	}
 
 	if (!backend_local) {
+#ifdef ENABLE_DSR
+		skb->cb[CB_SVC_PORT] = key.dport;
+		skb->cb[CB_SVC_ADDR_V6_1] = key.address.p1;
+		skb->cb[CB_SVC_ADDR_V6_2] = key.address.p2;
+		skb->cb[CB_SVC_ADDR_V6_3] = key.address.p3;
+		skb->cb[CB_SVC_ADDR_V6_4] = key.address.p4;
+		ep_tail_call(skb, CILIUM_CALL_IPV6_NODEPORT_DSR);
+		return DROP_MISSED_TAIL_CALL;
+#else
 		skb->cb[CB_NAT] = NAT_DIR_EGRESS;
 		ep_tail_call(skb, CILIUM_CALL_IPV6_NODEPORT_NAT);
 		return DROP_MISSED_TAIL_CALL;
+#endif /* ENABLE_DSR */
 	}
 
 	return TC_ACT_OK;
@@ -500,6 +692,165 @@ static __always_inline int nodeport_nat_ipv4_rev(struct __sk_buff *skb,
 	return NODEPORT_DO_NAT_IPV4(addr, NAT_DIR_INGRESS);
 }
 
+# ifdef ENABLE_DSR
+
+/* Helper function to set the IPv4 option for DSR when a backend is remote.
+ * NOTE: Revalidate data after calling the function.
+ */
+static __always_inline int set_dsr_opt4(struct __sk_buff *skb,
+					struct iphdr *ip4,
+					__be32 svc_addr, __be32 svc_port)
+{
+	union tcp_flags tcp_flags = { .value = 0 };
+	__be32 sum;
+	__u32 iph_old, iph_new;
+
+	if (ip4->protocol == IPPROTO_TCP) {
+		if (skb_load_bytes(skb, ETH_HLEN + sizeof(*ip4) + 12,
+				   &tcp_flags, 2) < 0)
+			return DROP_CT_INVALID_HDR;
+		// Setting the option is required only for the first packet
+		// (SYN), in the case of TCP, as for further packets of the
+		// same connection a remote node will use a NAT entry to
+		// reverse xlate a reply.
+		if (!(tcp_flags.value & (TCP_FLAG_SYN)))
+			return 0;
+	}
+
+	iph_old = *(__u32 *)ip4;
+	ip4->ihl += 0x2; // To accommodate u64 option
+	ip4->tot_len = bpf_htons(bpf_ntohs(ip4->tot_len) + 0x8);
+	iph_new = *(__u32 *)ip4;
+	__u32 opt1 = bpf_htonl(DSR_IPV4_OPT_32 | svc_port);
+	__u32 opt2 = bpf_htonl(svc_addr);
+
+	sum = csum_diff(&iph_old, 4, &iph_new, 4, 0);
+	sum = csum_diff(NULL, 0, &opt1, sizeof(opt1), sum);
+	sum = csum_diff(NULL, 0, &opt2, sizeof(opt2), sum);
+
+	if (skb_adjust_room(skb, 0x8, BPF_ADJ_ROOM_NET, 0))
+		return DROP_INVALID;
+	if (skb_store_bytes(skb, ETH_HLEN + sizeof(*ip4),
+			    &opt1, sizeof(opt1), 0) < 0)
+		return DROP_INVALID;
+	if (skb_store_bytes(skb, ETH_HLEN + sizeof(*ip4) + sizeof(opt1),
+			    &opt2, sizeof(opt2), 0) < 0)
+		return DROP_INVALID;
+
+	if (l3_csum_replace(skb, ETH_HLEN + offsetof(struct iphdr, check),
+	    0, sum, 0) < 0) {
+		return DROP_CSUM_L3;
+	}
+
+	return 0;
+}
+
+static __always_inline int handle_dsr_v4(struct __sk_buff *skb, bool *dsr)
+{
+	void *data, *data_end;
+	struct iphdr *ip4;
+
+	if (!revalidate_data(skb, &data, &data_end, &ip4))
+		return DROP_INVALID;
+
+	// Check whether IPv4 header contains a 64-bit option (IPv4 header
+	// w/o option (5 x 32-bit words) + the DSR option (2 x 32-bit words))
+	if (ip4->ihl == 0x7) {
+		uint32_t opt1 = 0;
+		uint32_t opt2 = 0;
+
+		if (skb_load_bytes(skb, ETH_HLEN + sizeof(struct iphdr),
+				   &opt1, sizeof(opt1)) < 0)
+			return DROP_INVALID;
+		opt1 = bpf_ntohl(opt1);
+
+		if ((opt1 & DSR_IPV4_OPT_MASK) == DSR_IPV4_OPT_32) {
+			if (skb_load_bytes(skb, ETH_HLEN +
+					   sizeof(struct iphdr) +
+					   sizeof(opt1),
+					   &opt2, sizeof(opt2)) < 0)
+				return DROP_INVALID;
+			opt2 = bpf_ntohl(opt2);
+
+			__be32 dport = opt1 & DSR_IPV4_DPORT_MASK;
+			__be32 address = opt2;
+			*dsr = true;
+
+			if (snat_v4_create_dsr(skb, address, dport) < 0) {
+				return DROP_INVALID;
+			}
+		}
+	}
+
+	return 0;
+}
+
+static __always_inline int xlate_dsr_v4(struct __sk_buff *skb,
+					struct ipv4_ct_tuple *tuple,
+					int l4_off)
+{
+	int ret;
+	struct ipv4_ct_tuple nat_tup = *tuple;
+	struct ipv4_nat_entry *entry;
+
+	nat_tup.flags = NAT_DIR_EGRESS;
+	nat_tup.sport = tuple->dport;
+	nat_tup.dport = tuple->sport;
+	entry = snat_v4_lookup(&nat_tup);
+	if (entry) {
+		ret = snat_v4_rewrite_egress(skb, &nat_tup, entry, l4_off);
+		if (ret != 0) {
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+__section_tail(CILIUM_MAP_CALLS, CILIUM_CALL_IPV4_NODEPORT_DSR)
+int tail_nodeport_ipv4_dsr(struct __sk_buff *skb)
+{
+	void *data, *data_end;
+	struct iphdr *ip4;
+	struct bpf_fib_lookup fib_params = {};
+	int ret;
+
+	__be16 dport = skb->cb[CB_SVC_PORT];
+	__be32 address = skb->cb[CB_SVC_ADDR_V4];
+
+	if (!revalidate_data(skb, &data, &data_end, &ip4))
+		return DROP_INVALID;
+
+	ret = set_dsr_opt4(skb, ip4, address, dport);
+	if (ret != 0)
+		return DROP_INVALID;
+
+	if (!revalidate_data(skb, &data, &data_end, &ip4))
+		return DROP_INVALID;
+
+	fib_params.family = AF_INET;
+	fib_params.ifindex = NATIVE_DEV_IFINDEX;
+	fib_params.ipv4_src = ip4->saddr;
+	fib_params.ipv4_dst = ip4->daddr;
+
+	ret = fib_lookup(skb, &fib_params, sizeof(fib_params),
+			 BPF_FIB_LOOKUP_DIRECT | BPF_FIB_LOOKUP_OUTPUT);
+	if (ret != 0) {
+		return DROP_NO_FIB;
+	}
+
+	if (eth_store_daddr(skb, fib_params.dmac, 0) < 0) {
+		return DROP_WRITE_ERROR;
+	}
+	if (eth_store_saddr(skb, fib_params.smac, 0) < 0) {
+		return DROP_WRITE_ERROR;
+	}
+
+	return redirect(fib_params.ifindex, 0);
+}
+
+# endif /*ENABLE_DSR */
+
 __section_tail(CILIUM_MAP_CALLS, CILIUM_CALL_IPV4_NODEPORT_NAT)
 int tail_nodeport_nat_ipv4(struct __sk_buff *skb)
 {
@@ -567,6 +918,7 @@ int tail_nodeport_nat_ipv4(struct __sk_buff *skb)
 	if (ifindex == ENCAP_IFINDEX)
 		goto out_send;
 #endif
+
 	if (!revalidate_data(skb, &data, &data_end, &ip4)) {
 		ret = DROP_INVALID;
 		goto drop_err;
@@ -618,7 +970,6 @@ static inline int nodeport_lb4(struct __sk_buff *skb, __u32 src_identity)
 	struct ct_state ct_state = {};
 	bool backend_local;
 	__u32 monitor = 0;
-	__u16 service_port;
 	union macaddr smac;
 
 	if (!revalidate_data(skb, &data, &data_end, &ip4))
@@ -648,11 +999,14 @@ static inline int nodeport_lb4(struct __sk_buff *skb, __u32 src_identity)
 	if (!svc || (!lb4_svc_is_external_ip(svc) && !lb4_svc_is_nodeport(svc))) {
 		if (svc)
 			return DROP_IS_CLUSTER_IP;
-
+#ifdef ENABLE_DSR
+		return TC_ACT_OK;
+#else
 		skb->cb[CB_NAT] = NAT_DIR_INGRESS;
 		skb->cb[CB_SRC_IDENTITY] = src_identity;
 		ep_tail_call(skb, CILIUM_CALL_IPV4_NODEPORT_NAT);
 		return DROP_MISSED_TAIL_CALL;
+#endif /* ENABLE_DSR */
 	}
 
 	ret = ct_lookup4(get_ct_map4(&tuple), &tuple, skb, l4_off, CT_EGRESS,
@@ -710,9 +1064,16 @@ redo:
 	}
 
 	if (!backend_local) {
+#ifdef ENABLE_DSR
+		skb->cb[CB_SVC_PORT] = key.dport;
+		skb->cb[CB_SVC_ADDR_V4] = key.address;
+		ep_tail_call(skb, CILIUM_CALL_IPV4_NODEPORT_DSR);
+		return DROP_MISSED_TAIL_CALL;
+#else // Fallback to SNAT
 		skb->cb[CB_NAT] = NAT_DIR_EGRESS;
 		ep_tail_call(skb, CILIUM_CALL_IPV4_NODEPORT_NAT);
 		return DROP_MISSED_TAIL_CALL;
+#endif /* ENABLE_DSR */
 	}
 
 	return TC_ACT_OK;
diff --git a/cilium/cmd/root.go b/cilium/cmd/root.go
index d1b8c3c4a2..51af340c0d 100644
--- a/cilium/cmd/root.go
+++ b/cilium/cmd/root.go
@@ -62,7 +62,8 @@ func init() {
 	flags.StringP("host", "H", "", "URI to server-side API")
 	viper.BindPFlags(flags)
 	rootCmd.AddCommand(newCmdCompletion(os.Stdout))
-	rootCmd.SetOutput(os.Stderr)
+	rootCmd.SetOut(os.Stderr)
+	rootCmd.SetErr(os.Stderr)
 }
 
 // initConfig reads in config file and ENV variables if set.
diff --git a/daemon/bpf.sha b/daemon/bpf.sha
index 4986eedd5b..212551275b 100644
--- a/daemon/bpf.sha
+++ b/daemon/bpf.sha
@@ -1,2 +1,2 @@
-GO_BINDATA_SHA1SUM=f53593794763e7d76e3adc59116c466af0c94e82
+GO_BINDATA_SHA1SUM=d9c731f4c00feef1c9117c611049dd9f4efb25ae
 BPF_FILES=../bpf/COPYING ../bpf/Makefile ../bpf/Makefile.bpf ../bpf/bpf_alignchecker.c ../bpf/bpf_features.h ../bpf/bpf_hostdev_ingress.c ../bpf/bpf_ipsec.c ../bpf/bpf_lxc.c ../bpf/bpf_netdev.c ../bpf/bpf_network.c ../bpf/bpf_overlay.c ../bpf/bpf_sock.c ../bpf/bpf_xdp.c ../bpf/cilium-map-migrate.c ../bpf/filter_config.h ../bpf/include/bpf/api.h ../bpf/include/elf/elf.h ../bpf/include/elf/gelf.h ../bpf/include/elf/libelf.h ../bpf/include/iproute2/bpf_elf.h ../bpf/include/linux/bpf.h ../bpf/include/linux/bpf_common.h ../bpf/include/linux/byteorder.h ../bpf/include/linux/byteorder/big_endian.h ../bpf/include/linux/byteorder/little_endian.h ../bpf/include/linux/icmp.h ../bpf/include/linux/icmpv6.h ../bpf/include/linux/if_arp.h ../bpf/include/linux/if_ether.h ../bpf/include/linux/if_packet.h ../bpf/include/linux/in.h ../bpf/include/linux/in6.h ../bpf/include/linux/ioctl.h ../bpf/include/linux/ip.h ../bpf/include/linux/ipv6.h ../bpf/include/linux/perf_event.h ../bpf/include/linux/swab.h ../bpf/include/linux/tcp.h ../bpf/include/linux/type_mapper.h ../bpf/include/linux/udp.h ../bpf/init.sh ../bpf/lib/arp.h ../bpf/lib/common.h ../bpf/lib/config.h ../bpf/lib/conntrack.h ../bpf/lib/conntrack_map.h ../bpf/lib/conntrack_test.h ../bpf/lib/csum.h ../bpf/lib/dbg.h ../bpf/lib/drop.h ../bpf/lib/encap.h ../bpf/lib/eps.h ../bpf/lib/eth.h ../bpf/lib/events.h ../bpf/lib/icmp6.h ../bpf/lib/ipv4.h ../bpf/lib/ipv6.h ../bpf/lib/ipv6_test.h ../bpf/lib/l3.h ../bpf/lib/l4.h ../bpf/lib/lb.h ../bpf/lib/lxc.h ../bpf/lib/maps.h ../bpf/lib/metrics.h ../bpf/lib/nat.h ../bpf/lib/nat46.h ../bpf/lib/nodeport.h ../bpf/lib/policy.h ../bpf/lib/signal.h ../bpf/lib/tailcall.h ../bpf/lib/trace.h ../bpf/lib/utils.h ../bpf/lib/xdp.h ../bpf/lxc_config.h ../bpf/netdev_config.h ../bpf/node_config.h ../bpf/probes/raw_change_tail.t ../bpf/probes/raw_fib_lookup.t ../bpf/probes/raw_insn.h ../bpf/probes/raw_invalidate_hash.t ../bpf/probes/raw_lpm_map.t ../bpf/probes/raw_lru_map.t ../bpf/probes/raw_main.c ../bpf/probes/raw_max_insn.t ../bpf/probes/raw_sock_cookie.t ../bpf/run_probes.sh ../bpf/sockops/Makefile ../bpf/sockops/bpf_redir.c ../bpf/sockops/bpf_sockops.c ../bpf/sockops/bpf_sockops.h ../bpf/sockops/sockops_config.h 
diff --git a/daemon/daemon_main.go b/daemon/daemon_main.go
index 9f0aa2da2f..96ceee6a26 100644
--- a/daemon/daemon_main.go
+++ b/daemon/daemon_main.go
@@ -481,6 +481,9 @@ func init() {
 	flags.Bool(option.EnableNodePort, false, "Enable NodePort type services by Cilium (beta)")
 	option.BindEnv(option.EnableNodePort)
 
+	flags.String(option.NodePortMode, defaults.NodePortMode, "BPF NodePort mode (\"snat\", \"dsr\")")
+	option.BindEnv(option.NodePortMode)
+
 	flags.StringSlice(option.NodePortRange, []string{fmt.Sprintf("%d", option.NodePortMinDefault), fmt.Sprintf("%d", option.NodePortMaxDefault)}, fmt.Sprintf("Set the min/max NodePort port range"))
 	option.BindEnv(option.NodePortRange)
 
@@ -1085,6 +1088,16 @@ func initEnv(cmd *cobra.Command) {
 		}
 	}
 
+	if option.Config.EnableNodePort {
+		if option.Config.NodePortMode != "dsr" && option.Config.NodePortMode != "snat" {
+			log.Fatalf("Invalid value for --node-port-mode option: %s", option.Config.NodePortMode)
+		}
+
+		if option.Config.NodePortMode == "dsr" && option.Config.Tunnel != option.TunnelDisabled {
+			log.Fatal("DSR cannot be used with tunnel")
+		}
+	}
+
 	// If device has been specified, use it to derive better default
 	// allocation prefixes
 	if option.Config.Device != "undefined" {
diff --git a/ginkgo.Jenkinsfile b/ginkgo.Jenkinsfile
index 81dc4233b9..c40061bdb1 100644
--- a/ginkgo.Jenkinsfile
+++ b/ginkgo.Jenkinsfile
@@ -133,6 +133,7 @@ pipeline {
                         TESTDIR="${GOPATH}/${PROJ_PATH}/test"
                         NETNEXT="true"
                         K8S_VERSION="1.11"
+                        //K8S_NODES="3"
                         KUBECONFIG="vagrant-kubeconfig"
                         CILIUM_REGISTRY="localnode" //setting it here so we don't compile Cilium in vagrant nodes (already done on local node registry)
                     }
diff --git a/install/kubernetes/cilium/charts/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index b9db6484fc..f1fe499ade 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -268,6 +268,9 @@ data:
 {{- if .Values.global.nodePort.device }}
   device: {{ .Values.global.nodePort.device | quote }}
 {{- end }}
+{{- if .Values.global.nodePort.mode }}
+  node-port-mode: {{ .Values.global.nodePort.mode | quote }}
+{{- end }}
 
 {{- end }}
 {{- if and .Values.global.pprof .Values.global.pprof.enabled }}
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index b47524c610..6f9bd24904 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -236,6 +236,9 @@ global:
     # device is the name of the device handling NodePort requests
     # device:
 
+    # dsr enables direct server return for NodePort services
+    dsr: false
+
   # flannel is the flannel specific configuration
   flannel:
     # enabled enables the flannel integration
diff --git a/jenkinsfiles/ginkgo-gke.Jenkinsfile b/jenkinsfiles/ginkgo-gke.Jenkinsfile
new file mode 100644
index 0000000000..901dec3241
--- /dev/null
+++ b/jenkinsfiles/ginkgo-gke.Jenkinsfile
@@ -0,0 +1,149 @@
+@Library('cilium') _
+
+pipeline {
+    agent {
+        label 'baremetal'
+    }
+
+    environment {
+        PROJ_PATH = "src/github.com/cilium/cilium"
+        GINKGO_TIMEOUT="108m"
+        TESTDIR="${WORKSPACE}/${PROJ_PATH}/test"
+        GOPATH="${WORKSPACE}"
+        GKE_KEY=credentials('gke-key')
+    }
+
+    options {
+        timeout(time: 240, unit: 'MINUTES')
+        timestamps()
+        ansiColor('xterm')
+    }
+
+    stages {
+        stage('Checkout') {
+            options {
+                timeout(time: 20, unit: 'MINUTES')
+            }
+
+            steps {
+                sh 'env'
+                checkout scm
+                sh 'mkdir -p ${PROJ_PATH}'
+                sh 'ls -A | grep -v src | xargs mv -t ${PROJ_PATH}'
+                sh '/usr/local/bin/cleanup || true'
+            }
+        }
+        stage('Precheck') {
+            options {
+                timeout(time: 20, unit: 'MINUTES')
+            }
+
+            steps {
+               sh "cd ${WORKSPACE}/${PROJ_PATH}; make jenkins-precheck"
+            }
+            post {
+               always {
+                   sh "cd ${WORKSPACE}/${PROJ_PATH}; make clean-jenkins-precheck || true"
+               }
+               unsuccessful {
+                   script {
+                       if  (!currentBuild.displayName.contains('fail')) {
+                           currentBuild.displayName = 'precheck fail\n' + currentBuild.displayName
+                       }
+                   }
+               }
+            }
+        }
+        stage('Authenticate in gke') {
+            steps {
+                dir("/tmp") {
+                    withCredentials([file(credentialsId: 'gke-key', variable: 'KEY_PATH')]) {
+                        sh 'gcloud auth activate-service-account --key-file ${KEY_PATH}'
+                    }
+                }
+            }
+        }
+        stage('Make Cilium images and prepare gke cluster') {
+            parallel {
+                stage('Make Cilium images') {
+                    environment {
+                        TESTDIR="${WORKSPACE}/${PROJ_PATH}/test"
+                    }
+                    steps {
+                        sh 'cd ${TESTDIR}; ./make-images-push-to-local-registry.sh $(./print-node-ip.sh) latest'
+                    }
+                    post {
+                        unsuccessful {
+                            script {
+                                if  (!currentBuild.displayName.contains('fail')) {
+                                    currentBuild.displayName = 'building or pushing Cilium images failed ' + currentBuild.displayName
+                                }
+                            }
+                        }
+                    }
+                }
+                stage ("Select cluster and scale it"){
+                    options {
+                        timeout(time: 20, unit: 'MINUTES')
+                    }
+                    environment {
+                        FAILFAST=setIfLabel("ci/fail-fast", "true", "false")
+                    }
+                    steps {
+                        dir("${TESTDIR}/gke") {
+                            sh './select-cluster.sh'
+                        }
+                    }
+                    post {
+                        unsuccessful {
+                            sh 'cd ${TESTDIR}/gke; ./release-cluster.sh'
+                            script {
+                                if  (!currentBuild.displayName.contains('fail')) {
+                                    currentBuild.displayName = 'Scaling cluster failed\n' + currentBuild.displayName
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        stage ("BDD-Test-PR"){
+            options {
+                timeout(time: 130, unit: 'MINUTES')
+            }
+            environment {
+                FAILFAST=setIfLabel("ci/fail-fast", "true", "false")
+                CONTAINER_RUNTIME=setIfLabel("area/containerd", "containerd", "docker")
+                KUBECONFIG="${TESTDIR}/gke/gke-kubeconfig"
+                CNI_INTEGRATION="gke"
+            }
+            steps {
+                dir("${TESTDIR}"){
+                    sh 'CILIUM_IMAGE=$(./print-node-ip.sh)/cilium/cilium:latest CILIUM_OPERATOR_IMAGE=$(./print-node-ip.sh)/cilium/operator:latest ginkgo --focus="$(echo ${ghprbCommentBody} | sed -r "s/([^ ]* |^[^ ]*$)//" | sed "s/^$/K8s*/")" -v --failFast=${FAILFAST} -- -cilium.provision=false -cilium.timeout=${GINKGO_TIMEOUT} -cilium.kubeconfig=${TESTDIR}/gke/gke-kubeconfig -cilium.passCLIEnvironment=true -cilium.registry=$(./print-node-ip.sh)'
+                }
+            }
+            post {
+                always {
+                    sh 'cd ${TESTDIR}/gke; ./release-cluster.sh'
+                    sh 'cd ${TESTDIR}; ./archive_test_results_eks.sh || true'
+                    archiveArtifacts artifacts: 'src/github.com/cilium/cilium/*.zip'
+                    junit testDataPublishers: [[$class: 'AttachmentPublisher']], testResults: 'src/github.com/cilium/cilium/test/*.xml'
+                }
+                unsuccessful {
+                    script {
+                        if  (!currentBuild.displayName.contains('fail')) {
+                            currentBuild.displayName = 'K8s tests fail\n' + currentBuild.displayName
+                        }
+                    }
+                }
+            }
+        }
+  }
+    post {
+        always {
+            sh 'cd ${TESTDIR}/gke; ./release-cluster.sh || true'
+            cleanWs()
+            sh '/usr/local/bin/cleanup || true'
+        }
+    }
+}
diff --git a/operator/cilium_node.go b/operator/cilium_node.go
index b87a001db2..d09358f954 100644
--- a/operator/cilium_node.go
+++ b/operator/cilium_node.go
@@ -42,14 +42,16 @@ func startSynchronizingCiliumNodes() {
 		cache.ResourceEventHandlerFuncs{
 			AddFunc: func(obj interface{}) {
 				if node, ok := obj.(*v2.CiliumNode); ok {
-					ciliumNodeUpdated(node.DeepCopy())
+					// node is deep copied before it is stored in pkg/aws/eni
+					ciliumNodeUpdated(node)
 				} else {
 					log.Warningf("Unknown CiliumNode object type %s received: %+v", reflect.TypeOf(obj), obj)
 				}
 			},
 			UpdateFunc: func(oldObj, newObj interface{}) {
 				if node, ok := newObj.(*v2.CiliumNode); ok {
-					ciliumNodeUpdated(node.DeepCopy())
+					// node is deep copied before it is stored in pkg/aws/eni
+					ciliumNodeUpdated(node)
 				} else {
 					log.Warningf("Unknown CiliumNode object type %s received: %+v", reflect.TypeOf(newObj), newObj)
 				}
diff --git a/operator/eni.go b/operator/eni.go
index 9044d0e5be..2a212610b1 100644
--- a/operator/eni.go
+++ b/operator/eni.go
@@ -79,6 +79,7 @@ func (k *k8sAPI) Update(node, origNode *v2.CiliumNode) (*v2.CiliumNode, error) {
 
 func ciliumNodeUpdated(resource *v2.CiliumNode) {
 	if nodeManager != nil {
+		// resource is deep copied before it is stored in pkg/aws/eni
 		nodeManager.Update(resource)
 	}
 }
diff --git a/pkg/aws/eni/node.go b/pkg/aws/eni/node.go
index c77a629b14..b2d886d3d5 100644
--- a/pkg/aws/eni/node.go
+++ b/pkg/aws/eni/node.go
@@ -207,6 +207,11 @@ func calculateExcessIPs(availableIPs, usedIPs, preAllocate, minAllocate, maxAbov
 }
 
 func (n *Node) updatedResource(resource *v2.CiliumNode) bool {
+	// Deep copy the resource before storing it. This way we are
+	// not dependent on caller not using the resource after this
+	// call.
+	resource = resource.DeepCopy()
+
 	n.mutex.Lock()
 	// Any modification to the custom resource is seen as a sign that the
 	// instance is alive
diff --git a/pkg/aws/eni/node_manager.go b/pkg/aws/eni/node_manager.go
index 1da207e504..669d4d534d 100644
--- a/pkg/aws/eni/node_manager.go
+++ b/pkg/aws/eni/node_manager.go
@@ -136,6 +136,7 @@ func (n *NodeManager) GetNames() (allNodeNames []string) {
 
 // Update is called whenever a CiliumNode resource has been updated in the
 // Kubernetes apiserver
+// 'resource' is deep copied before it is stored.
 func (n *NodeManager) Update(resource *v2.CiliumNode) bool {
 	n.mutex.Lock()
 	node, ok := n.nodes[resource.Name]
diff --git a/pkg/datapath/linux/config/config.go b/pkg/datapath/linux/config/config.go
index c9ad57c35e..403042b9c1 100644
--- a/pkg/datapath/linux/config/config.go
+++ b/pkg/datapath/linux/config/config.go
@@ -261,6 +261,10 @@ func (h *HeaderfileWriter) WriteNodeConfig(w io.Writer, cfg *datapath.LocalNodeC
 		if option.Config.EnableIPv6 {
 			cDefinesMap["NODEPORT_NEIGH6"] = "cilium_nodeport_neigh6"
 		}
+
+		if option.Config.NodePortMode == "dsr" {
+			cDefinesMap["ENABLE_DSR"] = "1"
+		}
 	}
 
 	// Since golang maps are unordered, we sort the keys in the map
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index d23b7d2c94..a968532fa1 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -340,4 +340,7 @@ const (
 
 	// EnableRemoteNodeIdentity is the default value for option.EnableRemoteNodeIdentity
 	EnableRemoteNodeIdentity = false
+
+	// NodePortMode is the default value for option.NodePortMode
+	NodePortMode = "snat"
 )
diff --git a/pkg/loadbalancer/loadbalancer.go b/pkg/loadbalancer/loadbalancer.go
index 0eb4f8d64f..8b014432a8 100644
--- a/pkg/loadbalancer/loadbalancer.go
+++ b/pkg/loadbalancer/loadbalancer.go
@@ -79,6 +79,18 @@ func (s ServiceFlags) IsSvcType(svcType SVCType) bool {
 	return s&CreateSvcFlag(svcType) != 0
 }
 
+// ServiceFlags returns a service type from the flags
+func (s ServiceFlags) SVCType() SVCType {
+	switch {
+	case s&serviceFlagExternalIPs != 0:
+		return SVCTypeExternalIPs
+	case s&serviceFlagNodePort != 0:
+		return SVCTypeNodePort
+	default:
+		return SVCTypeClusterIP
+	}
+}
+
 // String returns the string implementation of ServiceFlags.
 func (s ServiceFlags) String() string {
 	var strTypes []string
diff --git a/pkg/maps/lbmap/lbmap.go b/pkg/maps/lbmap/lbmap.go
index 0bb5308122..e8c25e3269 100644
--- a/pkg/maps/lbmap/lbmap.go
+++ b/pkg/maps/lbmap/lbmap.go
@@ -276,9 +276,7 @@ func (*LBBPFMap) DumpServiceMaps() ([]*loadbalancer.SVC, []error) {
 		addrStr := svc.Frontend.IP.String()
 		portStr := strconv.Itoa(int(svc.Frontend.Port))
 		host := net.JoinHostPort(addrStr, portStr)
-		if flagsCache[host].IsSvcType(loadbalancer.SVCTypeExternalIPs) {
-			svc.Type = loadbalancer.SVCTypeExternalIPs
-		}
+		svc.Type = flagsCache[host].SVCType()
 		newSVCList = append(newSVCList, &svc)
 	}
 
diff --git a/pkg/maps/nat/nat.go b/pkg/maps/nat/nat.go
index 020d576360..acd4945e64 100644
--- a/pkg/maps/nat/nat.go
+++ b/pkg/maps/nat/nat.go
@@ -220,11 +220,42 @@ func deleteMapping6(m *Map, ctKey *tuple.TupleKey6Global) error {
 	return nil
 }
 
+// Expects ingress tuple
+func deleteSwappedMapping4(m *Map, ctKey *tuple.TupleKey4Global) error {
+	key := NatKey4{TupleKey4Global: *ctKey}
+	// Because of #5848, we need to reverse only ports
+	port := key.SourcePort
+	key.SourcePort = key.DestPort
+	key.DestPort = port
+	key.Flags = tuple.TUPLE_F_OUT
+	m.Delete(&key)
+
+	return nil
+}
+
+// Expects ingress tuple
+func deleteSwappedMapping6(m *Map, ctKey *tuple.TupleKey6Global) error {
+	key := NatKey6{TupleKey6Global: *ctKey}
+	// Because of #5848, we need to reverse only ports
+	port := key.SourcePort
+	key.SourcePort = key.DestPort
+	key.DestPort = port
+	key.Flags = tuple.TUPLE_F_OUT
+	m.Delete(&key)
+
+	return nil
+}
+
 // DeleteMapping removes a NAT mapping from the global NAT table.
 func (m *Map) DeleteMapping(key tuple.TupleKey) error {
 	if key.GetFlags()&tuple.TUPLE_F_IN != 0 {
-		return nil
+		if m.v4 {
+			// To delete NAT entries created by DSR
+			return deleteSwappedMapping4(m, key.(*tuple.TupleKey4Global))
+		}
+		return deleteSwappedMapping6(m, key.(*tuple.TupleKey6Global))
 	}
+
 	if m.v4 {
 		return deleteMapping4(m, key.(*tuple.TupleKey4Global))
 	}
diff --git a/pkg/option/config.go b/pkg/option/config.go
index 6d5fb55a79..d1ba5524ac 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -237,6 +237,10 @@ const (
 	// EnableNodePort enables NodePort services implemented by Cilium in BPF
 	EnableNodePort = "enable-node-port"
 
+	// NodePortMode indicates in which mode NodePort implementation should run
+	// ("snat" or "dsr")
+	NodePortMode = "node-port-mode"
+
 	// NodePortRange defines a custom range where to look up NodePort services
 	NodePortRange = "node-port-range"
 
@@ -1236,6 +1240,10 @@ type DaemonConfig struct {
 	// EnableNodePort enables k8s NodePort service implementation in BPF
 	EnableNodePort bool
 
+	// NodePortMode indicates in which mode NodePort implementation should run
+	// ("snat" or "dsr")
+	NodePortMode string
+
 	// EnableK8sExternalIPs enables k8s external IPs implementation in BPF
 	EnableK8sExternalIPs bool
 
@@ -1657,6 +1665,7 @@ func (c *DaemonConfig) Populate() {
 	c.EnableL7Proxy = viper.GetBool(EnableL7Proxy)
 	c.EnableTracing = viper.GetBool(EnableTracing)
 	c.EnableNodePort = viper.GetBool(EnableNodePort)
+	c.NodePortMode = viper.GetString(NodePortMode)
 	c.EncryptInterface = viper.GetString(EncryptInterface)
 	c.EncryptNode = viper.GetBool(EncryptNode)
 	c.EnvoyLogPath = viper.GetString(EnvoyLog)
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index 9bd34683be..c1afdea1bb 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -390,19 +390,40 @@ func (l7 L7DataMap) addRulesForEndpoints(rules api.L7Rules, endpoints []CachedSe
 	}
 }
 
-func (l4 *L4Filter) getCerts(policyCtx PolicyContext, tls *api.TLSContext, direction string) *TLSContext {
+type TLSDirection string
+
+const (
+	TerminatingTLS TLSDirection = "terminating"
+	OriginatingTLS TLSDirection = "originating"
+)
+
+func (l4 *L4Filter) getCerts(policyCtx PolicyContext, tls *api.TLSContext, direction TLSDirection) (*TLSContext, error) {
 	if tls == nil {
-		return nil
+		return nil, nil
 	}
 	ca, public, private, err := policyCtx.GetTLSContext(tls)
 	if err != nil {
-		log.WithError(err).Warningf("policy: Error getting %s TLS Context, TLS will not work.", direction)
+		log.WithError(err).Warningf("policy: Error getting %s TLS Context.", direction)
+		return nil, err
+	}
+	switch direction {
+	case TerminatingTLS:
+		if public == "" || private == "" {
+			return nil, fmt.Errorf("Terminating TLS context is missing certs.")
+		}
+	case OriginatingTLS:
+		if ca == "" {
+			return nil, fmt.Errorf("Originating TLS context is missing CA certs.")
+		}
+	default:
+		return nil, fmt.Errorf("invalid TLS direction: %s", direction)
 	}
+
 	return &TLSContext{
 		TrustedCA:        ca,
 		CertificateChain: public,
 		PrivateKey:       private,
-	}
+	}, nil
 }
 
 // createL4Filter creates a filter for L4 policy that applies to the specified
@@ -411,7 +432,7 @@ func (l4 *L4Filter) getCerts(policyCtx PolicyContext, tls *api.TLSContext, direc
 // rules via the `rule` parameter.
 // Not called with an empty peerEndpoints.
 func createL4Filter(policyCtx PolicyContext, peerEndpoints api.EndpointSelectorSlice, rule api.PortRule, port api.PortProtocol,
-	protocol api.L4Proto, ruleLabels labels.LabelArray, ingress bool, fqdns api.FQDNSelectorSlice) *L4Filter {
+	protocol api.L4Proto, ruleLabels labels.LabelArray, ingress bool, fqdns api.FQDNSelectorSlice) (*L4Filter, error) {
 	selectorCache := policyCtx.GetSelectorCache()
 
 	// already validated via PortRule.Validate()
@@ -443,8 +464,15 @@ func createL4Filter(policyCtx PolicyContext, peerEndpoints api.EndpointSelectorS
 
 		// Note: No rules -> no TLS
 		if !rule.Rules.IsEmpty() {
-			terminatingTLS = l4.getCerts(policyCtx, rule.TerminatingTLS, "terminating")
-			originatingTLS = l4.getCerts(policyCtx, rule.OriginatingTLS, "originating")
+			var err error
+			terminatingTLS, err = l4.getCerts(policyCtx, rule.TerminatingTLS, TerminatingTLS)
+			if err != nil {
+				return nil, err
+			}
+			originatingTLS, err = l4.getCerts(policyCtx, rule.OriginatingTLS, OriginatingTLS)
+			if err != nil {
+				return nil, err
+			}
 		}
 
 		if protocol == api.ProtoTCP {
@@ -468,7 +496,7 @@ func createL4Filter(policyCtx PolicyContext, peerEndpoints api.EndpointSelectorS
 		}
 	}
 
-	return l4
+	return l4, nil
 }
 
 // detach releases the references held in the L4Filter and must be called before
@@ -497,9 +525,12 @@ func (l4 *L4Filter) attach(ctx PolicyContext, l4Policy *L4Policy) {
 // hostWildcardL7 determines if L7 traffic from Host should be
 // wildcarded (in the relevant daemon mode).
 func createL4IngressFilter(policyCtx PolicyContext, fromEndpoints api.EndpointSelectorSlice, hostWildcardL7 bool, rule api.PortRule, port api.PortProtocol,
-	protocol api.L4Proto, ruleLabels labels.LabelArray) *L4Filter {
+	protocol api.L4Proto, ruleLabels labels.LabelArray) (*L4Filter, error) {
 
-	filter := createL4Filter(policyCtx, fromEndpoints, rule, port, protocol, ruleLabels, true, nil)
+	filter, err := createL4Filter(policyCtx, fromEndpoints, rule, port, protocol, ruleLabels, true, nil)
+	if err != nil {
+		return nil, err
+	}
 
 	// If the filter would apply L7 rules for the Host, when we should accept everything from host,
 	// then wildcard Host at L7.
@@ -513,7 +544,7 @@ func createL4IngressFilter(policyCtx PolicyContext, fromEndpoints api.EndpointSe
 		}
 	}
 
-	return filter
+	return filter, nil
 }
 
 // createL4EgressFilter creates a filter for L4 policy that applies to the
@@ -521,7 +552,7 @@ func createL4IngressFilter(policyCtx PolicyContext, fromEndpoints api.EndpointSe
 // to the original rules that the filter is derived from. This filter may be
 // associated with a series of L7 rules via the `rule` parameter.
 func createL4EgressFilter(policyCtx PolicyContext, toEndpoints api.EndpointSelectorSlice, rule api.PortRule, port api.PortProtocol,
-	protocol api.L4Proto, ruleLabels labels.LabelArray, fqdns api.FQDNSelectorSlice) *L4Filter {
+	protocol api.L4Proto, ruleLabels labels.LabelArray, fqdns api.FQDNSelectorSlice) (*L4Filter, error) {
 
 	return createL4Filter(policyCtx, toEndpoints, rule, port, protocol, ruleLabels, false, fqdns)
 }
diff --git a/pkg/policy/rule.go b/pkg/policy/rule.go
index 981d1a884a..1b0c78b386 100644
--- a/pkg/policy/rule.go
+++ b/pkg/policy/rule.go
@@ -197,21 +197,22 @@ func mergePortProto(ctx *SearchContext, existingFilter, filterToMerge *L4Filter,
 // translated into L7 wildcards (ie, traffic will be forwarded to the
 // proxy for endpoints matching those labels, but the proxy will allow
 // all such traffic).
-func mergeIngressPortProto(policyCtx PolicyContext, ctx *SearchContext, endpoints api.EndpointSelectorSlice, hostWildcardL7 bool, r api.PortRule, p api.PortProtocol,
-	proto api.L4Proto, ruleLabels labels.LabelArray, resMap L4PolicyMap) (int, error) {
-	selectorCache := policyCtx.GetSelectorCache()
+func mergeIngressPortProto(policyCtx PolicyContext, ctx *SearchContext, endpoints api.EndpointSelectorSlice, hostWildcardL7 bool,
+	r api.PortRule, p api.PortProtocol, proto api.L4Proto, ruleLabels labels.LabelArray, resMap L4PolicyMap) (int, error) {
+	// Create a new L4Filter
+	filterToMerge, err := createL4IngressFilter(policyCtx, endpoints, hostWildcardL7, r, p, proto, ruleLabels)
+	if err != nil {
+		return 0, err
+	}
 
 	key := p.Port + "/" + string(proto)
 	existingFilter, ok := resMap[key]
 	if !ok {
-		resMap[key] = createL4IngressFilter(policyCtx, endpoints, hostWildcardL7, r, p, proto, ruleLabels)
+		resMap[key] = filterToMerge
 		return 1, nil
 	}
 
-	// Create a new L4Filter based off of the arguments provided to this function
-	// for merging with the filter which is already in the policy map.
-	filterToMerge := createL4IngressFilter(policyCtx, endpoints, hostWildcardL7, r, p, proto, ruleLabels)
-
+	selectorCache := policyCtx.GetSelectorCache()
 	if err := mergePortProto(ctx, existingFilter, filterToMerge, selectorCache); err != nil {
 		filterToMerge.detach(selectorCache)
 		return 0, err
@@ -586,19 +587,20 @@ func mergeEgress(policyCtx PolicyContext, ctx *SearchContext, toEndpoints api.En
 // L4PolicyMap for the specified port-protocol tuple, it returns an error.
 func mergeEgressPortProto(policyCtx PolicyContext, ctx *SearchContext, endpoints api.EndpointSelectorSlice, r api.PortRule, p api.PortProtocol,
 	proto api.L4Proto, ruleLabels labels.LabelArray, resMap L4PolicyMap, fqdns api.FQDNSelectorSlice) (int, error) {
-	selectorCache := policyCtx.GetSelectorCache()
+	// Create a new L4Filter
+	filterToMerge, err := createL4EgressFilter(policyCtx, endpoints, r, p, proto, ruleLabels, fqdns)
+	if err != nil {
+		return 0, err
+	}
 
 	key := p.Port + "/" + string(proto)
 	existingFilter, ok := resMap[key]
 	if !ok {
-		resMap[key] = createL4EgressFilter(policyCtx, endpoints, r, p, proto, ruleLabels, fqdns)
+		resMap[key] = filterToMerge
 		return 1, nil
 	}
 
-	// Create a new L4Filter based off of the arguments provided to this function
-	// for merging with the filter which is already in the policy map.
-	filterToMerge := createL4EgressFilter(policyCtx, endpoints, r, p, proto, ruleLabels, fqdns)
-
+	selectorCache := policyCtx.GetSelectorCache()
 	if err := mergePortProto(ctx, existingFilter, filterToMerge, selectorCache); err != nil {
 		filterToMerge.detach(selectorCache)
 		return 0, err
diff --git a/pkg/service/service.go b/pkg/service/service.go
index a358c0252c..b13ca5d1fc 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -506,9 +506,9 @@ func (s *Service) restoreServicesLocked() error {
 			frontend:      svc.Frontend,
 			backends:      svc.Backends,
 			backendByHash: map[string]*lb.Backend{},
-			// Correct service type and traffic policy will be restored by
-			// k8s_watcher after k8s service cache has been initialized
-			svcType:          lb.SVCTypeClusterIP,
+			// Correct traffic policy will be restored by k8s_watcher after k8s
+			// service cache has been initialized
+			svcType:          svc.Type,
 			svcTrafficPolicy: lb.SVCTrafficPolicyCluster,
 			// Indicate that the svc was restored from the BPF maps, so that
 			// SyncWithK8sFinished() could remove services which were restored


Test patch:
diff --git a/pkg/policy/l4_test.go b/pkg/policy/l4_test.go
index 53420a93ab..24fc0ed378 100644
--- a/pkg/policy/l4_test.go
+++ b/pkg/policy/l4_test.go
@@ -48,18 +48,53 @@ func (s *PolicyTestSuite) TestCreateL4Filter(c *C) {
 		// Regardless of ingress/egress, we should end up with
 		// a single L7 rule whether the selector is wildcarded
 		// or if it is based on specific labels.
-		filter := createL4IngressFilter(testPolicyContext, eps, false, portrule, tuple, tuple.Protocol, nil)
+		filter, err := createL4IngressFilter(testPolicyContext, eps, false, portrule, tuple, tuple.Protocol, nil)
+		c.Assert(err, IsNil)
 		c.Assert(len(filter.L7RulesPerEp), Equals, 1)
 		c.Assert(filter.IsEnvoyRedirect(), Equals, true)
 		c.Assert(filter.IsProxylibRedirect(), Equals, false)
 
-		filter = createL4EgressFilter(testPolicyContext, eps, portrule, tuple, tuple.Protocol, nil, nil)
+		filter, err = createL4EgressFilter(testPolicyContext, eps, portrule, tuple, tuple.Protocol, nil, nil)
+		c.Assert(err, IsNil)
 		c.Assert(len(filter.L7RulesPerEp), Equals, 1)
 		c.Assert(filter.IsEnvoyRedirect(), Equals, true)
 		c.Assert(filter.IsProxylibRedirect(), Equals, false)
 	}
 }
 
+func (s *PolicyTestSuite) TestCreateL4FilterMissingSecret(c *C) {
+	tuple := api.PortProtocol{Port: "80", Protocol: api.ProtoTCP}
+	portrule := api.PortRule{
+		Ports: []api.PortProtocol{tuple},
+		TerminatingTLS: &api.TLSContext{
+			Secret: &api.Secret{
+				Name: "notExisting",
+			},
+		},
+		Rules: &api.L7Rules{
+			HTTP: []api.PortRuleHTTP{
+				{Path: "/public", Method: "GET"},
+			},
+		},
+	}
+	selectors := []api.EndpointSelector{
+		api.NewESFromLabels(),
+		api.NewESFromLabels(labels.ParseSelectLabel("bar")),
+	}
+
+	for _, selector := range selectors {
+		eps := []api.EndpointSelector{selector}
+		// Regardless of ingress/egress, we should end up with
+		// a single L7 rule whether the selector is wildcarded
+		// or if it is based on specific labels.
+		_, err := createL4IngressFilter(testPolicyContext, eps, false, portrule, tuple, tuple.Protocol, nil)
+		c.Assert(err, Not(IsNil))
+
+		_, err = createL4EgressFilter(testPolicyContext, eps, portrule, tuple, tuple.Protocol, nil, nil)
+		c.Assert(err, Not(IsNil))
+	}
+}
+
 type SortablePolicyRules []*models.PolicyRule
 
 func (a SortablePolicyRules) Len() int           { return len(a) }
diff --git a/test/ginkgo-ext/scopes.go b/test/ginkgo-ext/scopes.go
index eab8aa4356..a645770f76 100644
--- a/test/ginkgo-ext/scopes.go
+++ b/test/ginkgo-ext/scopes.go
@@ -558,3 +558,14 @@ func SkipContextIf(condition func() bool, text string, body func()) bool {
 
 	return Context(text, body)
 }
+
+// SkipItIf executes the given body if the given condition is NOT met.
+func SkipItIf(condition func() bool, text string, body func()) bool {
+	if condition() {
+		return It(text, func() {
+			Skip("skipping due to unmet condition")
+		})
+	}
+
+	return It(text, body)
+}
diff --git a/test/gke/lock.yaml b/test/gke/lock.yaml
new file mode 100644
index 0000000000..0680e3e0a1
--- /dev/null
+++ b/test/gke/lock.yaml
@@ -0,0 +1,17 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: lock
+spec:
+  replicas: 0
+  selector:
+    matchLabels:
+      app: lock
+  template:
+    metadata:
+      labels:
+        app: lock
+    spec:
+      containers:
+      - name: lock
+        image: gcr.io/google_containers/pause-amd64:3.0
diff --git a/test/gke/release-cluster.sh b/test/gke/release-cluster.sh
new file mode 100755
index 0000000000..cec1098b52
--- /dev/null
+++ b/test/gke/release-cluster.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+set -e
+
+zone=europe-west4-a
+export KUBECONFIG=gke-kubeconfig
+
+cluster=$(cat cluster-name)
+echo "scaling $cluster ng to 0"
+yes | gcloud container clusters resize $cluster --node-pool default-pool --num-nodes 0 --zone $zone
+
+echo "releasing cluster lock from $cluster"
+kubectl annotate deployment lock lock-
+
+rm cluster-name
diff --git a/test/gke/select-cluster.sh b/test/gke/select-cluster.sh
new file mode 100755
index 0000000000..cd65bc30cd
--- /dev/null
+++ b/test/gke/select-cluster.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+set -e
+
+locked=1
+
+zone=europe-west4-a
+export KUBECONFIG=gke-kubeconfig
+
+while [ $locked -ne 0 ]; do
+    rm gke-kubeconfig || true
+    echo "selecting random cluster"
+    cluster=$(gcloud container clusters list --zone europe-west4-a | grep cilium-ci | sort -R | head -n 1 | awk '{print $1}')
+
+    echo "getting kubeconfig for $cluster"
+    gcloud container clusters get-credentials --zone $zone $cluster
+
+    echo "aquiring cluster lock"
+    set +e
+    kubectl create -f lock.yaml
+
+    kubectl annotate deployment lock lock=1
+    locked=$?
+    echo $locked
+    set -e
+done
+
+echo "lock acquired on cluster $cluster"
+echo $cluster > cluster-name
+
+echo "creating cilium ns"
+kubectl create ns cilium || true
+
+echo "scaling $cluster to 2"
+yes | gcloud container clusters resize $cluster --node-pool default-pool --num-nodes 2 --zone $zone
+
+echo "labeling nodes"
+index=1
+for node in $(kubectl get nodes --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}');
+do
+    kubectl label node $node cilium.io/ci-node=k8s$index
+    index=$((index+1))
+done
+
+echo "adding node registry as trusted"
+helm template ../k8sT/manifests/registry-adder-gke --set IP="$(../print-node-ip.sh)" > registry-adder.yaml
+kubectl apply -f registry-adder.yaml
diff --git a/test/helpers/cons.go b/test/helpers/cons.go
index 84851470a1..24af0e2dcb 100644
--- a/test/helpers/cons.go
+++ b/test/helpers/cons.go
@@ -58,6 +58,7 @@ const (
 	K8s     = "k8s"
 	K8s1    = "k8s1"
 	K8s2    = "k8s2"
+	K8s3    = "k8s3"
 	Runtime = "runtime"
 
 	Enabled  = "enabled"
diff --git a/test/helpers/kubectl.go b/test/helpers/kubectl.go
index b65e003a60..790934fa36 100644
--- a/test/helpers/kubectl.go
+++ b/test/helpers/kubectl.go
@@ -242,6 +242,7 @@ func CreateKubectl(vmName string, log *logrus.Entry) (k *Kubectl) {
 func (kub *Kubectl) LabelNodes() {
 	kub.ExecMiddle(fmt.Sprintf("%s label --overwrite node k8s1 cilium.io/ci-node=k8s1", KubectlCmd))
 	kub.ExecMiddle(fmt.Sprintf("%s label --overwrite node k8s2 cilium.io/ci-node=k8s2", KubectlCmd))
+	kub.ExecMiddle(fmt.Sprintf("%s label --overwrite node k8s3 cilium.io/ci-node=k8s3", KubectlCmd))
 }
 
 // CepGet returns the endpoint model for the given pod name in the specified
diff --git a/test/helpers/utils.go b/test/helpers/utils.go
index c49f46f4f7..b1f8ffcc85 100644
--- a/test/helpers/utils.go
+++ b/test/helpers/utils.go
@@ -24,6 +24,7 @@ import (
 	"math/rand"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"syscall"
 	"time"
@@ -457,6 +458,18 @@ func DoesNotRunOnNetNext() bool {
 	return !RunsOnNetNext()
 }
 
+// DoesNotHaveHosts returns a function which returns true if a CI job
+// has less VMs than the given count.
+func DoesNotHaveHosts(count int) func() bool {
+	return func() bool {
+		if c, err := strconv.Atoi(os.Getenv("K8S_NODES")); err != nil {
+			return true
+		} else {
+			return c < count
+		}
+	}
+}
+
 // CiliumDevImage returns cilium docker image name based on cilium.registry option and const CiliumDevImage
 func CiliumDevImage() string {
 	return fmt.Sprintf(ciliumDeveloperImage, config.CiliumTestConfig.Registry)
diff --git a/test/k8sT/DatapathConfiguration.go b/test/k8sT/DatapathConfiguration.go
index f9de65ae1e..fa283a75fb 100644
--- a/test/k8sT/DatapathConfiguration.go
+++ b/test/k8sT/DatapathConfiguration.go
@@ -209,7 +209,7 @@ var _ = Describe("K8sDatapathConfig", func() {
 			ExpectWithOffset(1, err).Should(BeNil(), "Unable to determine cilium pod on node %s", helpers.K8s1)
 			status := kubectl.CiliumExec(ciliumPod, "cilium bpf tunnel list | wc -l")
 			status.ExpectSuccess()
-			Expect(status.IntOutput()).Should(Equal(3), "Did not find expected number of entries in BPF tunnel map")
+			Expect(status.IntOutput()).Should(Equal((kubectl.GetNumNodes()-1)*2+1), "Did not find expected number of entries in BPF tunnel map")
 		}
 
 		It("Check connectivity with transparent encryption and VXLAN encapsulation", func() {
diff --git a/test/k8sT/Policies.go b/test/k8sT/Policies.go
index 81635c0b72..61468a6790 100644
--- a/test/k8sT/Policies.go
+++ b/test/k8sT/Policies.go
@@ -89,6 +89,7 @@ var _ = Describe("K8sPolicyTest", func() {
 		knpAllowEgress = helpers.ManifestGet(kubectl.BasePath(), "knp-default-allow-egress.yaml")
 		cnpMatchExpression = helpers.ManifestGet(kubectl.BasePath(), "cnp-matchexpressions.yaml")
 
+		deleteCiliumDS(kubectl)
 		DeployCiliumOptionsAndDNS(kubectl, []string{
 			"--set global.tls.secretsBackend=k8s",
 			"--set global.debug.verbose=flow",
diff --git a/test/k8sT/Services.go b/test/k8sT/Services.go
index 65336d0288..68c2961884 100644
--- a/test/k8sT/Services.go
+++ b/test/k8sT/Services.go
@@ -220,6 +220,11 @@ var _ = Describe("K8sServicesTest", func() {
 			testHTTPRequest(testDSClient, url)
 		})
 
+		getURL := func(host string, port int32) string {
+			return fmt.Sprintf("http://%s",
+				net.JoinHostPort(host, fmt.Sprintf("%d", port)))
+		}
+
 		doRequests := func(url string, count int, fromPod string) {
 			By("Making %d HTTP requests from %s to %q", count, fromPod, url)
 			for i := 1; i <= count; i++ {
@@ -240,26 +245,31 @@ var _ = Describe("K8sServicesTest", func() {
 			}
 		}
 
-		getURL := func(host string, port int32) string {
-			return fmt.Sprintf("http://%s",
-				net.JoinHostPort(host, fmt.Sprintf("%d", port)))
-		}
-
-		doRequestsFromOutsideClient := func(url string, count int, checkSourceIP bool) {
-			ssh := helpers.GetVagrantSSHMeta(helpers.K8s1VMName())
-			By("Making %d HTTP requests from outside cluster to %q", count, url)
-			for i := 1; i <= count; i++ {
-				cmd := helpers.CurlFail(url)
-				if checkSourceIP {
-					cmd += " | grep client_address="
-				}
-				res := ssh.ContainerExec("client-from-outside", cmd)
-				ExpectWithOffset(1, res).Should(helpers.CMDSuccess(),
-					"Can not connect to service %q from outside cluster", url)
-				if checkSourceIP {
-					Expect(strings.TrimSpace(strings.Split(res.GetStdOut(), "=")[1])).To(Equal("192.168.10.10"))
+		doRequestsFromThirdHostWithLocalPort :=
+			func(url string, count int, checkSourceIP bool, fromPort int) {
+				var cmd string
+				By("Making %d HTTP requests from outside cluster to %q", count, url)
+				for i := 1; i <= count; i++ {
+					if fromPort == 0 {
+						cmd = helpers.CurlFail(url)
+					} else {
+						cmd = helpers.CurlFail("--local-port %d %s", fromPort, url)
+					}
+					if checkSourceIP {
+						cmd += " | grep client_address="
+					}
+					k8s3Name, k8s3IP := getNodeInfo(helpers.K8s3)
+					res, err := kubectl.ExecInHostNetNS(context.TODO(), k8s3Name, cmd)
+					Expect(err).Should(BeNil(), "Cannot exec in k8s3 host netns")
+					ExpectWithOffset(1, res).Should(helpers.CMDSuccess(),
+						"Can not connect to service %q from outside cluster", url)
+					if checkSourceIP {
+						Expect(strings.TrimSpace(strings.Split(res.GetStdOut(), "=")[1])).To(Equal(k8s3IP))
+					}
 				}
 			}
+		doRequestsFromThirdHost := func(url string, count int, checkSourceIP bool) {
+			doRequestsFromThirdHostWithLocalPort(url, count, checkSourceIP, 0)
 		}
 
 		testNodePort := func(bpfNodePort bool) {
@@ -330,10 +340,10 @@ var _ = Describe("K8sServicesTest", func() {
 
 			count := 10
 			url := getURL(k8s1IP, data.Spec.Ports[0].NodePort)
-			doRequestsFromOutsideClient(url, count, true)
+			doRequestsFromThirdHost(url, count, true)
 
-			// Checks that requests to k8s1 succeed, while requests to k8s2 are dropped
-			err = kubectl.Get(helpers.DefaultNamespace, "service test-nodeport-local-k8s1").Unmarshal(&data)
+			// Checks that requests to k8s2 succeed, while requests to k8s1 are dropped
+			err = kubectl.Get(helpers.DefaultNamespace, "service test-nodeport-local-k8s2").Unmarshal(&data)
 			Expect(err).Should(BeNil(), "Can not retrieve service")
 
 			url = getURL(k8s1IP, data.Spec.Ports[0].NodePort)
@@ -349,7 +359,7 @@ var _ = Describe("K8sServicesTest", func() {
 			testNodePort(false)
 		})
 
-		It("Tests NodePort (kube-proxy) with externalTrafficPolicy=Local", func() {
+		SkipItIf(helpers.DoesNotHaveHosts(3), "Tests NodePort (kube-proxy) with externalTrafficPolicy=Local", func() {
 			testExternalTrafficPolicyLocal()
 		})
 
@@ -374,99 +384,139 @@ var _ = Describe("K8sServicesTest", func() {
 			})
 		})
 
-		SkipContextIf(helpers.DoesNotRunOnNetNext, "Tests NodePort BPF", func() {
-			// TODO(brb) Add with L7 policy test cases after GH#8971 has been fixed
+		SkipContextIf(
+			func() bool { return helpers.DoesNotRunOnNetNext() || helpers.DoesNotHaveHosts(3)() },
+			"Tests NodePort BPF", func() {
+				// TODO(brb) Add with L7 policy test cases after GH#8971 has been fixed
 
-			nativeDev := "enp0s8"
+				nativeDev := "enp0s8"
 
-			BeforeAll(func() {
-				enableBackgroundReport = false
-			})
-
-			AfterAll(func() {
-				enableBackgroundReport = true
-				// Remove NodePort programs (GH#8873)
-				pods, err := kubectl.GetCiliumPods(helpers.KubeSystemNamespace)
-				Expect(err).To(BeNil(), "Cannot retrieve Cilium pods")
-				for _, pod := range pods {
-					ret := kubectl.CiliumExec(pod, "tc filter del dev "+nativeDev+" ingress")
-					Expect(ret.WasSuccessful()).Should(BeTrue(), "Cannot remove ingress bpf_netdev on %s", pod)
-					ret = kubectl.CiliumExec(pod, "tc filter del dev "+nativeDev+" egress")
-					Expect(ret.WasSuccessful()).Should(BeTrue(), "Cannot remove egress bpf_netdev on %s", pod)
-				}
-				deleteCiliumDS(kubectl)
-				// Deploy Cilium as the next test expects it to be up and running
-				DeployCiliumAndDNS(kubectl)
-			})
-
-			Context("Tests with vxlan", func() {
 				BeforeAll(func() {
-					deleteCiliumDS(kubectl)
-					DeployCiliumOptionsAndDNS(kubectl, []string{
-						"--set global.nodePort.enabled=true",
-						"--set global.nodePort.device=" + nativeDev,
-					})
+					enableBackgroundReport = false
 				})
 
-				It("Tests NodePort", func() {
-					testNodePort(true)
+				AfterAll(func() {
+					enableBackgroundReport = true
+					// Remove NodePort programs (GH#8873)
+					pods, err := kubectl.GetCiliumPods(helpers.KubeSystemNamespace)
+					Expect(err).To(BeNil(), "Cannot retrieve Cilium pods")
+					for _, pod := range pods {
+						ret := kubectl.CiliumExec(pod, "tc filter del dev "+nativeDev+" ingress")
+						Expect(ret.WasSuccessful()).Should(BeTrue(), "Cannot remove ingress bpf_netdev on %s", pod)
+						ret = kubectl.CiliumExec(pod, "tc filter del dev "+nativeDev+" egress")
+						Expect(ret.WasSuccessful()).Should(BeTrue(), "Cannot remove egress bpf_netdev on %s", pod)
+					}
+					deleteCiliumDS(kubectl)
+					// Deploy Cilium as the next test expects it to be up and running
+					DeployCiliumAndDNS(kubectl)
 				})
 
-				It("Tests NodePort with externalTrafficPolicy=Local", func() {
-					testExternalTrafficPolicyLocal()
-				})
-			})
+				Context("Tests with vxlan", func() {
+					BeforeAll(func() {
+						deleteCiliumDS(kubectl)
+						DeployCiliumOptionsAndDNS(kubectl, []string{
+							"--set global.nodePort.enabled=true",
+							"--set global.nodePort.device=" + nativeDev,
+						})
+					})
 
-			Context("Tests with direct routing", func() {
-				BeforeAll(func() {
-					deleteCiliumDS(kubectl)
-					DeployCiliumOptionsAndDNS(kubectl, []string{
-						"--set global.nodePort.enabled=true",
-						"--set global.nodePort.device=" + nativeDev,
-						"--set global.tunnel=disabled",
-						"--set global.autoDirectNodeRoutes=true",
+					It("Tests NodePort", func() {
+						testNodePort(true)
 					})
-				})
 
-				It("Tests NodePort", func() {
-					testNodePort(true)
+					It("Tests NodePort with externalTrafficPolicy=Local", func() {
+						testExternalTrafficPolicyLocal()
+					})
 				})
 
-				It("Tests NodePort with externalTrafficPolicy=Local", func() {
-					testExternalTrafficPolicyLocal()
-				})
-			})
+				Context("Tests with direct routing", func() {
+					BeforeAll(func() {
+						deleteCiliumDS(kubectl)
+						DeployCiliumOptionsAndDNS(kubectl, []string{
+							"--set global.nodePort.enabled=true",
+							"--set global.nodePort.device=" + nativeDev,
+							"--set global.tunnel=disabled",
+							"--set global.autoDirectNodeRoutes=true",
+						})
+					})
 
-			Context("Tests with MetalLB", func() {
-				var (
-					metalLB string
-				)
+					It("Tests NodePort", func() {
+						testNodePort(true)
+					})
 
-				BeforeAll(func() {
-					// Will allocate LoadBalancer IPs from 192.168.36.{240-250} range
-					metalLB = helpers.ManifestGet(kubectl.BasePath(), "metallb.yaml")
-					res := kubectl.ApplyDefault(metalLB)
-					res.ExpectSuccess("Unable to apply %s", metalLB)
+					It("Tests NodePort with externalTrafficPolicy=Local", func() {
+						testExternalTrafficPolicyLocal()
+					})
 				})
 
-				AfterAll(func() {
-					_ = kubectl.Delete(metalLB)
+				Context("Tests with MetalLB", func() {
+					var (
+						metalLB string
+					)
+
+					BeforeAll(func() {
+						// Will allocate LoadBalancer IPs from 192.168.36.{240-250} range
+						metalLB = helpers.ManifestGet(kubectl.BasePath(), "metallb.yaml")
+						res := kubectl.ApplyDefault(metalLB)
+						res.ExpectSuccess("Unable to apply %s", metalLB)
+					})
+
+					AfterAll(func() {
+						_ = kubectl.Delete(metalLB)
+					})
+
+					It("Connectivity to endpoint via LB", func() {
+						lbIP, err := kubectl.GetLoadBalancerIP(
+							helpers.DefaultNamespace, "test-lb", 30*time.Second)
+						Expect(err).Should(BeNil(), "Cannot retrieve loadbalancer IP for test-lb")
+
+						k8s1Name, _ := getNodeInfo(helpers.K8s1)
+						k8s2Name, _ := getNodeInfo(helpers.K8s2)
+						doRequestsFromThirdHost("http://"+lbIP, 10, false)
+						doRequests("http://"+lbIP, 10, k8s1Name)
+						doRequests("http://"+lbIP, 10, k8s2Name)
+					})
 				})
 
-				It("Connectivity to endpoint via LB", func() {
-					lbIP, err := kubectl.GetLoadBalancerIP(
-						helpers.DefaultNamespace, "test-lb", 30*time.Second)
-					Expect(err).Should(BeNil(), "Cannot retrieve loadbalancer IP for test-lb")
+				It("Tests with direct routing and DSR", func() {
+					deleteCiliumDS(kubectl)
+					DeployCiliumOptionsAndDNS(kubectl, []string{
+						"--set global.nodePort.enabled=true",
+						"--set global.nodePort.device=" + nativeDev,
+						"--set global.nodePort.mode=dsr",
+						"--set global.tunnel=disabled",
+						"--set global.autoDirectNodeRoutes=true",
+					})
 
-					k8s1Name, _ := getNodeInfo(helpers.K8s1)
-					k8s2Name, _ := getNodeInfo(helpers.K8s2)
-					doRequestsFromOutsideClient("http://"+lbIP, 10, false)
-					doRequests("http://"+lbIP, 10, k8s1Name)
-					doRequests("http://"+lbIP, 10, k8s2Name)
+					var data v1.Service
+					err := kubectl.Get(helpers.DefaultNamespace, "service test-nodeport").Unmarshal(&data)
+					Expect(err).Should(BeNil(), "Cannot retrieve service")
+					_, k8s1IP := getNodeInfo(helpers.K8s1)
+					url := getURL(k8s1IP, data.Spec.Ports[0].NodePort)
+					doRequestsFromThirdHost(url, 10, true)
+
+					// Test whether DSR NAT entries are evicted by GC
+
+					pod, err := kubectl.GetCiliumPodOnNode(helpers.KubeSystemNamespace, helpers.K8s2)
+					Expect(err).Should(BeNil(), fmt.Sprintf("Cannot determine cilium pod name"))
+					// "test-nodeport-k8s2" because we want to trigger SNAT with a single request:
+					// client -> k8s1 -> endpoint @ k8s2.
+					err = kubectl.Get(helpers.DefaultNamespace, "service test-nodeport-k8s2").Unmarshal(&data)
+					Expect(err).Should(BeNil(), "Cannot retrieve service")
+					url = getURL(k8s1IP, data.Spec.Ports[0].NodePort)
+
+					doRequestsFromThirdHostWithLocalPort(url, 1, true, 64000)
+					res := kubectl.CiliumExec(pod, "cilium bpf nat list | grep 64000")
+					Expect(res.GetStdOut()).ShouldNot(BeEmpty(), "NAT entry was not evicted")
+					res.ExpectSuccess("Unable to list NAT entries")
+					// Flush CT maps to trigger eviction of the NAT entries (simulates CT GC)
+					res = kubectl.CiliumExec(pod, "cilium bpf ct flush global")
+					res.ExpectSuccess("Unable to flush CT maps")
+					res = kubectl.CiliumExec(pod, "cilium bpf nat list | grep 64000")
+					res.ExpectSuccess("Unable to list NAT entries")
+					Expect(res.GetStdOut()).Should(BeEmpty(), "NAT entry was not evicted")
 				})
 			})
-		})
-
 	})
 
 	//TODO: Check service with IPV6
diff --git a/test/k8sT/manifests/demo_ds.yaml b/test/k8sT/manifests/demo_ds.yaml
index fae6458510..7510f6b45d 100644
--- a/test/k8sT/manifests/demo_ds.yaml
+++ b/test/k8sT/manifests/demo_ds.yaml
@@ -13,6 +13,17 @@ spec:
       labels:
         zgroup: testDS
     spec:
+      # Some tests (e.g. DSR) requires to send requests to testds from a host
+      # which does not run any of its pods. We've chosen k8s3 to be such host.
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: "cilium.io/ci-node"
+                operator: NotIn
+                values:
+                - k8s3
       containers:
       - name: web
         image: docker.io/cilium/echoserver:1.10
@@ -57,16 +68,16 @@ spec:
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: test-k8s1
+  name: test-k8s2
   namespace: default
 spec:
   selector:
     matchLabels:
-      zgroup: test-k8s1
+      zgroup: test-k8s2
   template:
     metadata:
       labels:
-        zgroup: test-k8s1
+        zgroup: test-k8s2
     spec:
       containers:
       - name: web
@@ -80,7 +91,7 @@ spec:
             port: 80
       terminationGracePeriodSeconds: 0
       nodeSelector:
-        "cilium.io/ci-node": k8s1
+        "cilium.io/ci-node": k8s2
 ---
 apiVersion: v1
 kind: Service
@@ -125,7 +136,7 @@ spec:
 apiVersion: v1
 kind: Service
 metadata:
-  name: test-nodeport-local-k8s1
+  name: test-nodeport-local-k8s2
 spec:
   type: NodePort
   externalTrafficPolicy: Local
@@ -135,7 +146,21 @@ spec:
     protocol: TCP
     name: http
   selector:
-    zgroup: test-k8s1
+    zgroup: test-k8s2
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: test-nodeport-k8s2
+spec:
+  type: NodePort
+  ports:
+  - port: 10080
+    targetPort: 80
+    protocol: TCP
+    name: http
+  selector:
+    zgroup: test-k8s2
 ---
 apiVersion: v1
 kind: Service
diff --git a/test/k8sT/manifests/registry-adder-gke/.helmignore b/test/k8sT/manifests/registry-adder-gke/.helmignore
new file mode 100644
index 0000000000..50af031725
--- /dev/null
+++ b/test/k8sT/manifests/registry-adder-gke/.helmignore
@@ -0,0 +1,22 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/test/k8sT/manifests/registry-adder-gke/Chart.yaml b/test/k8sT/manifests/registry-adder-gke/Chart.yaml
new file mode 100644
index 0000000000..ba369338f0
--- /dev/null
+++ b/test/k8sT/manifests/registry-adder-gke/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+appVersion: "1.0"
+description: A Helm chart for Kubernetes
+name: registry-adder
+version: 0.1.0
diff --git a/test/k8sT/manifests/registry-adder-gke/templates/_helpers.tpl b/test/k8sT/manifests/registry-adder-gke/templates/_helpers.tpl
new file mode 100644
index 0000000000..a0c03e4abe
--- /dev/null
+++ b/test/k8sT/manifests/registry-adder-gke/templates/_helpers.tpl
@@ -0,0 +1,45 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "registry-adder.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "registry-adder.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "registry-adder.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Common labels
+*/}}
+{{- define "registry-adder.labels" -}}
+app.kubernetes.io/name: {{ include "registry-adder.name" . }}
+helm.sh/chart: {{ include "registry-adder.chart" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end -}}
diff --git a/test/k8sT/manifests/registry-adder-gke/templates/daemonset.yaml b/test/k8sT/manifests/registry-adder-gke/templates/daemonset.yaml
new file mode 100644
index 0000000000..0f6791b138
--- /dev/null
+++ b/test/k8sT/manifests/registry-adder-gke/templates/daemonset.yaml
@@ -0,0 +1,48 @@
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  namespace: kube-system
+  name: {{ include "registry-adder.fullname" . }}
+  labels:
+{{ include "registry-adder.labels" . | indent 4 }}
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: {{ include "registry-adder.name" . }}
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: {{ include "registry-adder.name" . }}
+        app.kubernetes.io/instance: {{ .Release.Name }}
+    spec:
+      containers:
+      - env:
+        # STARTUP_SCRIPT is the script run on node bootstrap. Node
+        # bootstrapping can be customized in this script.
+        - name: STARTUP_SCRIPT
+          value: |
+            if grep -Fxq "{{ .Values.IP }}" /etc/default/docker
+            then
+              echo "found node ip in docker config, skipping docker restart"
+            else
+              sed -i 's;--insecure-registry;--insecure-registry {{ .Values.IP }} --insecure-registry;g' /etc/default/docker
+              systemctl restart docker
+            fi
+        image: docker.io/cilium/startup-script:v1
+        imagePullPolicy: IfNotPresent
+        name: registry-adder
+        securityContext:
+          capabilities:
+            add:
+            - NET_ADMIN
+            - SYS_MODULE
+            - SYS_ADMIN
+          privileged: true
+      dnsPolicy: ClusterFirstWithHostNet
+      hostNetwork: true
+      hostPID: true
+      priorityClassName: system-node-critical
+      terminationGracePeriodSeconds: 1
+      tolerations:
+      - operator: Exists
diff --git a/test/k8sT/manifests/registry-adder-gke/values.yaml b/test/k8sT/manifests/registry-adder-gke/values.yaml
new file mode 100644
index 0000000000..0cf03660cd
--- /dev/null
+++ b/test/k8sT/manifests/registry-adder-gke/values.yaml
@@ -0,0 +1,5 @@
+# Default values for registry-adder.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+IP: 0.0.0.0
diff --git a/test/k8sT/manifests/registry-adder/templates/daemonset.yaml b/test/k8sT/manifests/registry-adder/templates/daemonset.yaml
index add0f0ae39..cd8ce5056b 100644
--- a/test/k8sT/manifests/registry-adder/templates/daemonset.yaml
+++ b/test/k8sT/manifests/registry-adder/templates/daemonset.yaml
@@ -28,7 +28,7 @@ spec:
             cat /etc/docker/daemon.json | cat /etc/docker/daemon.json | jq '. + {"insecure-registries": ["{{ .Values.IP }}"]}' | cat > tmp.json
             cat tmp.json > /etc/docker/daemon.json
             pgrep dockerd | xargs kill -SIGHUP
-        image: docker.io/cilium/startup-script:v1
+        image: docker.io/cilium/startup-script:v3
         imagePullPolicy: IfNotPresent
         name: registry-adder
         securityContext:
@@ -51,5 +51,5 @@ spec:
       volumes:
       - hostPath:
           path: /etc/docker
-          type: DirectoryOrCreate
+          type: Directory
         name: docker-config
diff --git a/test/provision/k8s_install.sh b/test/provision/k8s_install.sh
index a5fe153679..b78736cea7 100755
--- a/test/provision/k8s_install.sh
+++ b/test/provision/k8s_install.sh
@@ -342,19 +342,4 @@ docker network create --subnet=192.168.9.0/24 outside
 docker run --net outside --ip 192.168.9.10 --restart=always -d docker.io/cilium/demo-httpd:latest
 docker run --net outside --ip 192.168.9.11 --restart=always -d docker.io/cilium/demo-httpd:latest
 
-if [[ "${HOST}" == "k8s1" ]]; then
-    # To avoid SNAT'ing source IP, we create a network with masquerading disabled.
-    # Also, we install a route on each other node to make it possible a replies from
-    # remote nodes to reach containers attached to the network.
-    docker network create --subnet=192.168.10.0/24 \
-        --opt 'com.docker.network.bridge.enable_ip_masquerade=false' \
-        outside-no-masq
-    # NOTE: when changing "client-from-outside" IP addr, make sure that the IP addr
-    # is changed in the tests (grep for the IP addr).
-    docker run --name client-from-outside --net outside-no-masq --ip 192.168.10.10 \
-        --restart=always -d docker.io/cilium/demo-client:latest
-else
-    sudo ip route add 192.168.10.0/24 via 192.168.36.11 || true
-fi
-
 sudo touch /etc/provision_finished
diff --git a/test/vagrant-ci-start.sh b/test/vagrant-ci-start.sh
index f766fe73a2..ccf85a5db8 100755
--- a/test/vagrant-ci-start.sh
+++ b/test/vagrant-ci-start.sh
@@ -2,11 +2,17 @@
 
 set -e
 
+K8S_NODES=${K8S_NODES:-2}
+
 echo "destroying vms in case this is a retry"
-vagrant destroy k8s1-${K8S_VERSION} k8s2-${K8S_VERSION} --force
+for i in $(seq 1 $K8S_NODES); do
+    vagrant destroy k8s${i}-${K8S_VERSION} --force
+done
 
 echo "starting vms"
-vagrant up k8s1-${K8S_VERSION} k8s2-${K8S_VERSION} --provision
+for i in $(seq 1 $K8S_NODES); do
+    vagrant up k8s${i}-${K8S_VERSION} --provision
+done
 
 echo "getting vagrant kubeconfig from provisioned vagrant cluster"
 ./get-vagrant-kubeconfig.sh > vagrant-kubeconfig
@@ -19,5 +25,6 @@ helm template k8sT/manifests/registry-adder --set IP="$(./print-node-ip.sh)" > r
 kubectl apply -f registry-adder.yaml
 
 echo "labeling nodes"
-kubectl label node k8s1 cilium.io/ci-node=k8s1
-kubectl label node k8s2 cilium.io/ci-node=k8s2
+for i in $(seq 1 $K8S_NODES); do
+    kubectl label node k8s${i} cilium.io/ci-node=k8s${i}
+done