Repository: cilium/cilium
Pull request #9896 (2020-01-17 17:05:33)

Restore SVC type after restart
Since the b4e0402 "bpf, nodeport: add nodeport flag to nodeport services" commit we started to set a nodeport flag in the BPF LB maps. Because of that, we can determine service types when restore them from the maps.

This helps when running cilium-agent without k8s, as in this case a non-ClusterIP service type is lost after cilium-agent has been restarted.

There still one discrepancy: LoadBalancer type services are stored as ExternalIP in the BPF maps, so these service types are properly restored after receiving corresponding updates from k8s.

<!-- 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/9896)
<!-- Reviewable:end -->


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

Coverage decreased (-0.002%) to 45.885% when pulling **5425c16a0e2222740678d36bce5cd85c359934b6 on pr/brb/fix-restore-svc-type** into **ff54dbd703b6f30140a2671c3e03074bfa4642ed on master**.

brb: test-me-please

Patch:
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/bpf_lxc.c b/bpf/bpf_lxc.c
index 2a4579a9f0..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);
@@ -556,20 +563,9 @@ ct_recreate4:
 		}
 # ifdef ENABLE_DSR
 		if (ct_state.dsr) {
-			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;
-				}
-			}
+			ret = xlate_dsr_v4(skb, &tuple, l4_off);
+			if (ret != 0)
+				return ret;
 		}
 # endif /* ENABLE_DSR */
 #endif /* ENABLE_NODEPORT */
@@ -878,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;
@@ -1000,7 +1006,6 @@ ipv4_policy(struct __sk_buff *skb, int ifindex, __u32 src_label, __u8 *reason, _
 	__be32 orig_dip, orig_sip;
 	bool is_fragment = false;
 	__u32 monitor = 0;
-	bool dsr = false;
 
 	if (!revalidate_data(skb, &data, &data_end, &ip4))
 		return DROP_INVALID;
@@ -1068,43 +1073,18 @@ ipv4_policy(struct __sk_buff *skb, int ifindex, __u32 src_label, __u8 *reason, _
 
 	if (ret == CT_NEW) {
 #ifdef ENABLE_DSR
-		if (!revalidate_data(skb, &data, &data_end, &ip4))
-			return DROP_INVALID;
+		bool dsr = false;
 
-		// 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;
-				}
-			}
-		}
+		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;
-		ct_state_new.dsr = dsr;
 		ret = ct_create4(get_ct_map4(&tuple), &tuple, skb, CT_INGRESS, &ct_state_new, verdict > 0);
 		if (IS_ERR(ret))
 			return ret;
diff --git a/bpf/lib/common.h b/bpf/lib/common.h
index 47cd3158f7..eb8493f24d 100644
--- a/bpf/lib/common.h
+++ b/bpf/lib/common.h
@@ -90,7 +90,8 @@
 #define CILIUM_CALL_IPV6_NODEPORT_REVNAT	18
 #define CILIUM_CALL_ENCAP_NODEPORT_NAT		19
 #define CILIUM_CALL_IPV4_NODEPORT_DSR		20
-#define CILIUM_CALL_SIZE			21
+#define CILIUM_CALL_IPV6_NODEPORT_DSR		21
+#define CILIUM_CALL_SIZE			22
 
 typedef __u64 mac_t;
 
@@ -397,6 +398,18 @@ enum {
 #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
  */
@@ -472,13 +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 */
+#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_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 */
diff --git a/bpf/lib/conntrack.h b/bpf/lib/conntrack.h
index 11ab63bea9..df1c926bfc 100644
--- a/bpf/lib/conntrack.h
+++ b/bpf/lib/conntrack.h
@@ -667,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;
diff --git a/bpf/lib/nat.h b/bpf/lib/nat.h
index df77aeabe0..42afca5bd8 100644
--- a/bpf/lib/nat.h
+++ b/bpf/lib/nat.h
@@ -893,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 608ede083f..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)
 {
@@ -269,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,
@@ -331,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;
@@ -499,11 +692,14 @@ 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 inline int set_dsr_opt4(struct __sk_buff *skb, struct iphdr *ip4,
-			       __be32 svc_addr, __be32 svc_port)
+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;
@@ -549,6 +745,68 @@ static inline int set_dsr_opt4(struct __sk_buff *skb, struct iphdr *ip4,
 	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)
 {
@@ -591,6 +849,8 @@ int tail_nodeport_ipv4_dsr(struct __sk_buff *skb)
 	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)
 {
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 f64e76a0b8..212551275b 100644
--- a/daemon/bpf.sha
+++ b/daemon/bpf.sha
@@ -1,2 +1,2 @@
-GO_BINDATA_SHA1SUM=3e5d95fce4421334acbc9130086d5943fd5afd76
+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 612f21846c..96ceee6a26 100644
--- a/daemon/daemon_main.go
+++ b/daemon/daemon_main.go
@@ -1093,13 +1093,8 @@ func initEnv(cmd *cobra.Command) {
 			log.Fatalf("Invalid value for --node-port-mode option: %s", option.Config.NodePortMode)
 		}
 
-		if option.Config.NodePortMode == "dsr" {
-			switch {
-			case option.Config.EnableIPv6:
-				log.Fatal("DSR cannot be used with IPv6")
-			case option.Config.Tunnel != option.TunnelDisabled:
-				log.Fatal("DSR cannot be used with tunnel")
-			}
+		if option.Config.NodePortMode == "dsr" && option.Config.Tunnel != option.TunnelDisabled {
+			log.Fatal("DSR cannot be used with tunnel")
 		}
 	}
 
diff --git a/ginkgo.Jenkinsfile b/ginkgo.Jenkinsfile
index a0d9595c3d..c40061bdb1 100644
--- a/ginkgo.Jenkinsfile
+++ b/ginkgo.Jenkinsfile
@@ -133,7 +133,7 @@ pipeline {
                         TESTDIR="${GOPATH}/${PROJ_PATH}/test"
                         NETNEXT="true"
                         K8S_VERSION="1.11"
-                        K8S_NODES="3"
+                        //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/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 25dd124068..acd4945e64 100644
--- a/pkg/maps/nat/nat.go
+++ b/pkg/maps/nat/nat.go
@@ -233,6 +233,19 @@ func deleteSwappedMapping4(m *Map, ctKey *tuple.TupleKey4Global) error {
 	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 {
@@ -240,6 +253,7 @@ func (m *Map) DeleteMapping(key tuple.TupleKey) error {
 			// To delete NAT entries created by DSR
 			return deleteSwappedMapping4(m, key.(*tuple.TupleKey4Global))
 		}
+		return deleteSwappedMapping6(m, key.(*tuple.TupleKey6Global))
 	}
 
 	if m.v4 {
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/k8sT/Services.go b/test/k8sT/Services.go
index 4c80b93d73..68c2961884 100644
--- a/test/k8sT/Services.go
+++ b/test/k8sT/Services.go
@@ -486,7 +486,6 @@ var _ = Describe("K8sServicesTest", func() {
 						"--set global.nodePort.mode=dsr",
 						"--set global.tunnel=disabled",
 						"--set global.autoDirectNodeRoutes=true",
-						"--set global.ipv6.enabled=false",
 					})
 
 					var data v1.Service
@@ -509,15 +508,12 @@ var _ = Describe("K8sServicesTest", func() {
 					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")
-					// TODO(brb) Uncomment all "res.ExpectSuccess()" after adding
-					//           IPv6 DSR support (cilium bpf {ct,nat} cmds exit with 1
-					//           due to missing ipv6 maps).
-					// res.ExpectSuccess("Unable to list NAT entries")
+					res.ExpectSuccess("Unable to list NAT entries")
 					// Flush CT maps to trigger eviction of the NAT entries (simulates CT GC)
-					kubectl.CiliumExec(pod, "cilium bpf ct flush global")
-					// res.ExpectSuccess("Unable to flush CT maps")
+					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")
+					res.ExpectSuccess("Unable to list NAT entries")
 					Expect(res.GetStdOut()).Should(BeEmpty(), "NAT entry was not evicted")
 				})
 			})