datapath: add tail call hooks for custom metrics, bytecounter example

Custom Metrics
Byte counters! Let's count how much data is received and sent for each endpoint. Related PR: #13173
But more than byte counters, this PR introduces hooks in the datapath so that users can tail call into custom programs, and collect all kind of metrics they want - Provided they implement it themselves in eBPF. Here is an overview:

First commit introduces the tail call hooks in the datapath (IPv4 only), plus management for the per-endpoint prog_array maps used to reference the custom programs. Those maps have two entries, one for ingress and one for egress.

Second commit makes these tail calls optional, and opt-in, through the introduction of a dedicated Cilium option.

Third commit adds a sample application: a simple byte counter which can be attached to keep track of the amount of data received or sent for a given endpoint.

Fourth commit adds the tail call hooks to the IPv6 datapath, on the same model as for IPv4.

To use it, once Cilium is deployed:

Compile the custom programmake -C bpf/custom

Load itbpftool prog load bpf/custom/bpf_custom.o /sys/fs/bpf/tc/globals/bytecounter type classifier \
        pinmaps /sys/fs/bpf/tc/globals/bytecounter_maps

Reference it from the prog_array mapEP=<endpoint_id>
bpftool map update pinned /sys/fs/bpf/tc/globals/cilium_calls_custom_$(printf '%05d' ${EP}) \
        key 0 0 0 0 value pinned /sys/fs/bpf/tc/globals/bytecounter              # or key `1 0 0 0` for egress

For now, dump counters' value manuallybpftool map dump pinned /sys/fs/bpf/tc/globals/bytecounter_maps/bytecount_map

PR Status
Here are some points of interest:

I am still trying to finalise the location for the ingress hook. In the current version, the tail call added to tail_ipv4_policy() is not always executed, as handle_policy() (which calls it) is skipped in ipv4_local_delivery() if USE_BPF_PROG_FOR_INGRESS_POLICY is defined i.e. if enable-endpoint-routes is set to true. This is the case for GKE, for example. So my current plan is to add the tail call to the different branches in ipv4_local_delivery(), possibly by replacing returns with goto in that function to write the tail call just once, and also possibly with preprocessor guards to make sure it is only called from bpf_lxc.

The custom program needs to return with the same value the original program would have used, to preserve the datapath logics. This means that we must pass the desired value from bpf_lxc to the custom program we hook into. I'm using cb[CB_CT_STATE] for that because it did not seem to be used at that location, but I might be wrong. Feedback on this approach (or an alternatives) would be welcome.

I also pass the current direction (ingress or egress) and the source identity to the custom program. Given that direction needs 1 bit, identity needs 24, and existing return values are low and needs less than 7 bits, I make it all fit on the same cb cell.

Once we have the counters in an eBPF map, we would need to extract and process it. It could be a Cilium metric, but I'm not sure how to implement custom, user-defined metrics. It could be just from the command line, but similarly this requires to give a way to users to specify what kind of metrics they want. At the moment, the values from the counters can be retrieved from bpftool.

test-me-please

test-me-please

test-1.20-4.9

test-1.20-4.9

Marking as ready for reviews again.
All feedback from the previous reviews should be addressed (I know, it was some time ago, took me longer than expected, sorry reviewers!).

I had a general question around how users can debug the custom tail call hooks when things are not working as expected.
This is outside of the scope of this PR so I will address this in a follow-up PR as needed, but I wanted to get some feedback first.
The per endpoint custom tail calls map has ingress/egress(ipv4/ipv6) as the keys and custom BPF program fd as the value. How does a user map this fd to the relevant custom program? AFAIK, there is no command line utility available (e.g., bpftool) that wraps BPF_OBJ_GET syscall to get the program fd from its pinned path. Is that right? Do we want to expose a Cilium cli command to get this debug information?

The per endpoint custom tail calls map has ingress/egress(ipv4/ipv6) as the keys and custom BPF program fd as the value. How does a user map this fd to the relevant custom program? AFAIK, there is no command line utility available (e.g., bpftool) that wraps BPF_OBJ_GET syscall to get the program fd from its pinned path. Is that right? Do we want to expose a Cilium cli command to get this debug information?

Ideally, I think we should teach bpftool to print the target BPF program (its ID or a summary of info?) instead of the fd.

Ideally, I think we should teach bpftool to print the target BPF program (its ID or a summary of info?) instead of the fd.

bpftool map dump <some prog array map> will already show the ids of the programs referenced in the map. Does that answer Aditi's question, too?
(bpftool map lookup should do the same I suppose, but on my setup it currently complains about a failure to get BTF info and refuses to give the entry, possibly a bug to investigate.)

I agree with Aditi on the suggested items for CI tests.
As discussed offline, I plan to start with a rather simple test:

Deploy some pods
Attach the byte-counter program added in this PR to one pod
Send some traffic, possibly ICMP
Check that we can access the values for the counter and that they are consistent.
Repeat with different direction / IP version.

This may be enough to validate the PR (open to discussion). There are other tests that we could do, possibly as follow-ups. I'm thinking, for example: Adding some L7 rules before sending the traffic and examining the counters, to make sure that the guard on ret != CTX_ACT_TX is effective and that we don't process the packet multiple times.

No CI test yet (so not asking for new reviews). The changes should simply address Paul's review.
Incremental diff
diff --git a/bpf/Makefile b/bpf/Makefile
index 117a13f4658a..0c224c8588a1 100644
--- a/bpf/Makefile
+++ b/bpf/Makefile
@@ -236,7 +236,8 @@ LXC_OPTIONS = \
 MAX_LXC_OPTIONS = $(MAX_BASE_OPTIONS) -DENCAP_IFINDEX=1
 ifneq ("$(KERNEL)","49")
 MAX_LXC_OPTIONS += -DENABLE_IPV4_FRAGMENTS=1 -DENABLE_NODEPORT=1 -DENABLE_SESSION_AFFINITY=1 \
-	-DENABLE_EXTERNAL_IP=1 -DENABLE_HOSTPORT=1 -DENABLE_LOADBALANCER=1
+	-DENABLE_EXTERNAL_IP=1 -DENABLE_HOSTPORT=1 -DENABLE_LOADBALANCER=1 \
+	-DENABLE_CUSTOM_CALLS
 endif

 bpf_lxc.ll: bpf_lxc.c $(LIB)
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 40bc5117cee9..a2d2e1183f8d 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -462,9 +462,11 @@ int tail_handle_ipv6(struct __ctx_buff *ctx)
 					CTX_ACT_DROP, METRIC_EGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, dstID))
+	if (!encode_custom_prog_meta(ctx, ret, dstID)) {
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV6_EGRESS);
+		return DROP_MISSED_TAIL_CALL;
+	}
 #endif

 	return ret;
@@ -806,9 +808,11 @@ int tail_handle_ipv4(struct __ctx_buff *ctx)
 					CTX_ACT_DROP, METRIC_EGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, dstID))
+	if (!encode_custom_prog_meta(ctx, ret, dstID)) {
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV4_EGRESS);
+		return DROP_MISSED_TAIL_CALL;
+	}
 #endif

 	return ret;
@@ -1075,9 +1079,17 @@ int tail_ipv6_policy(struct __ctx_buff *ctx)
 	ctx_store_meta(ctx, CB_PROXY_MAGIC, ctx->mark);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (ret != CTX_ACT_TX && !encode_custom_prog_meta(ctx, ret, src_label))
+	/* Make sure we skip the tail call when the packet is being redirected
+	 * to a L7 proxy, to avoid running the custom program twice on the
+	 * incoming packet (before redirecting, and on the way back from the
+	 * proxy).
+	 */
+	if (ret != CTX_ACT_TX &&
+	    !encode_custom_prog_meta(ctx, ret, src_label)) {
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV6_INGRESS);
+		return DROP_MISSED_TAIL_CALL;
+	}
 #endif

 	return ret;
@@ -1142,9 +1154,11 @@ out:
 					ret, CTX_ACT_DROP, METRIC_INGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, src_identity))
+	if (!encode_custom_prog_meta(ctx, ret, src_identity)) {
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV6_INGRESS);
+		return DROP_MISSED_TAIL_CALL;
+	}
 #endif

 	return ret;
@@ -1346,9 +1360,17 @@ int tail_ipv4_policy(struct __ctx_buff *ctx)
 	ctx_store_meta(ctx, CB_PROXY_MAGIC, ctx->mark);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (ret != CTX_ACT_TX && !encode_custom_prog_meta(ctx, ret, src_label))
+	/* Make sure we skip the tail call when the packet is being redirected
+	 * to a L7 proxy, to avoid running the custom program twice on the
+	 * incoming packet (before redirecting, and on the way back from the
+	 * proxy).
+	 */
+	if (ret != CTX_ACT_TX &&
+	    !encode_custom_prog_meta(ctx, ret, src_label)) {
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV4_INGRESS);
+		return DROP_MISSED_TAIL_CALL;
+	}
 #endif

 	return ret;
@@ -1412,9 +1434,11 @@ out:
 					ret, CTX_ACT_DROP, METRIC_INGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, src_identity))
+	if (!encode_custom_prog_meta(ctx, ret, src_identity)) {
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV4_INGRESS);
+		return DROP_MISSED_TAIL_CALL;
+	}
 #endif

 	return ret;

bpftool map dump <some prog array map> will already show the ids of the programs referenced in the map. Does that answer Aditi's question, too?
(bpftool map lookup should do the same I suppose, but on my setup it currently complains about a failure to get BTF info and refuses to give the entry, possibly a bug to investigate.)

Not really.

How does a user map this fd to the relevant custom program?

There doesn't seem to be a command line utility that prints all of this information (program fd/id/pinned path) together.

The per endpoint custom tail calls map has ingress/egress(ipv4/ipv6) as the keys and custom BPF program fd as the value. How does a user map this fd to the relevant custom program? AFAIK, there is no command line utility available (e.g., bpftool) that wraps BPF_OBJ_GET syscall to get the program fd from its pinned path. Is that right? Do we want to expose a Cilium cli command to get this debug information?

bpftool map dump  will already show the ids of the programs referenced in the map. Does that answer Aditi's question, too?

There doesn't seem to be a command line utility that prints all of this information (program fd/id/pinned path) together.

File descriptors are unique per process (only shared with child processes), so it doesn't make sense to read the integers directly and somehow interpret them across both cilium-agent and bpftool output. What we can do is dump the prog_array map and get the prog ids associated with them. That id can be directly inspected further using @pchaigno's instructions.
# bpftool map dump pinned /sys/fs/bpf/tc/globals/cilium_call_policy
key: d4 01 00 00  value: 73 03 00 00
key: be 02 00 00  value: 7f 03 00 00
key: 3a 0c 00 00  value: 74 03 00 00
Found 3 elements
# bpftool prog dump xlated id 0xabcd > /dev/null && echo "success." || echo "failed."
Error: get by id (43981): No such file or directory
failed.
# bpftool prog dump xlated id 0x0373 > /dev/null && echo "success." || echo "failed."
success.
# bpftool prog dump xlated id 0x037f > /dev/null && echo "success." || echo "failed."
success.
# bpftool prog dump xlated id 0x0374 > /dev/null && echo "success." || echo "failed."
success.

I filtered out the xlated instructions output above since that's irrelevant to the point, but if we wanted to inspect the actual program/instructions for the program, we would use the xlated prog command like above.
Kernel v4.15 introduced the prog_name field into the BPF_PROG_LOAD bpf() syscall subcommand, so if we made use of that then I think we could make the output a little easier to interpret when using bpftool prog list. I've filed https://github.com/cilium/cilium/issues/15229 to specifically follow up on that.

Could we move the sample program outside the tree / bpf core? (For example, could be in different repo that would git module the cilium/cilium one for the C lib functions.)
Similarly, we also moved out the health probe example from the LB-only code: https://github.com/cilium/health-examples

Could we move the sample program outside the tree / bpf core? (For example, could be in different repo that would git module the cilium/cilium one for the C lib functions.)

@borkmann how do you do CI for that if some of the code is out-of-tree?

Could we move the sample program outside the tree / bpf core? (For example, could be in different repo that would git module the cilium/cilium one for the C lib functions.)

@borkmann how do you do CI for that if some of the code is out-of-tree?

From the description on To use it, once Cilium is deployed: we could have the prog stand-alone compileable to avoid users pulling in more of Cilium lib/ internal headers and relying on them, and then have it shipped as a container for the CI, for example?
I guess my question is, do we prefer to have a big catch-all mono repo or components that are not strictly necessary in the main code base like sample code (this, this or this) better live elsewhere?
Given I saw existing example code also being outside, I went with the same for the health client and assumed that would be preferred?

   Could we move the sample program outside the tree / bpf core? (For example, could be in different repo that would git module the cilium/cilium one for the C lib functions.)

@borkmann how do you do CI for that if some of the code is out-of-tree?

From the description on To use it, once Cilium is deployed: we could have the prog stand-alone compileable to avoid users pulling in more of Cilium lib/ internal headers and relying on them, and then have it shipped as a container for the CI, for example?
I guess my question is, do we prefer to have a big catch-all mono repo or components that are not strictly necessary in the main code base like sample code (this, this or this) better live elsewhere?
Given I saw existing example code also being outside, I went with the same for the health client and assumed that would be preferred?

Sure, that could work - separate repo with the sample code + Makefile/Dockerfile, then we'll just manually build/tag/push a docker image for the sample code that can be pulled into the CI as part of a new YAML DaemonSet to validate. I'm also fine with proposing another approach if @qmonnet has other ideas how this can work.

New additions:

CI test!
Some documentation. Because the feature is very low-level, it was decided with Joe to simply have a README file next to the sample byte counter.
Header file containing the core of the custom program can be passed via a dedicated Makefile variable, instead of editing file bpf_custom.c for updating the #include directive.

Please see the relevant commit log messages for details.
Most comments from the last round of reviews should have been addressed.
The one standing out is Daniel's request to package the example program externally. I'm working on it, but would like to leave it as a short-term follow-up PR.
Incremental diff
The three following files are new (and left out of the diff):

bpf/custom/README.rst
test/k8sT/CustomCalls.go
test/k8sT/manifests/demo-customcalls.yaml

diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index 6492c0383580..d2dcf56a6355 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -57,9 +57,10 @@ static __always_inline bool redirect_to_proxy(int verdict, __u8 dir)
 /* Encode return value and identity into cb buffer. This is used before
  * executing tail calls to custom programs. "ret" is the return value supposed
  * to be returned to the kernel, needed by the callee to preserve the datapath
- * logics. The "identity" reflects the local endpoint: the source of the packet
- * on ingress path, or its destination on the egress path. We encode it so that
- * custom programs can retrieve it and use it at their convenience.
+ * logics. The "identity" is the security identity of the local endpoint: the
+ * source of the packet on ingress path, or its destination on the egress path.
+ * We encode it so that custom programs can retrieve it and use it at their
+ * convenience.
  */
 static __always_inline int
 encode_custom_prog_meta(struct __ctx_buff *ctx, int ret, __u32 identity)
@@ -475,11 +476,9 @@ int tail_handle_ipv6(struct __ctx_buff *ctx)
 					CTX_ACT_DROP, METRIC_EGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, dstID)) {
+	if (!encode_custom_prog_meta(ctx, ret, dstID))
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV6_EGRESS);
-		return DROP_MISSED_TAIL_CALL;
-	}
 #endif

 	return ret;
@@ -833,11 +832,9 @@ int tail_handle_ipv4(struct __ctx_buff *ctx)
 					CTX_ACT_DROP, METRIC_EGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, dstID)) {
+	if (!encode_custom_prog_meta(ctx, ret, dstID))
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV4_EGRESS);
-		return DROP_MISSED_TAIL_CALL;
-	}
 #endif

 	return ret;
@@ -1088,6 +1085,9 @@ int tail_ipv6_policy(struct __ctx_buff *ctx)
 	int ret, ifindex = ctx_load_meta(ctx, CB_IFINDEX);
 	__u32 src_label = ctx_load_meta(ctx, CB_SRC_LABEL);
 	bool from_host = ctx_load_meta(ctx, CB_FROM_HOST);
+#ifdef ENABLE_CUSTOM_CALLS
+	bool proxy_redirect = false;
+#endif
 	__u16 proxy_port = 0;
 	__u8 reason = 0;

@@ -1096,8 +1096,12 @@ int tail_ipv6_policy(struct __ctx_buff *ctx)

 	ret = ipv6_policy(ctx, ifindex, src_label, &reason, &tuple,
 			  &proxy_port, from_host);
-	if (ret == POLICY_ACT_PROXY_REDIRECT)
+	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy6(ctx, &tuple, proxy_port, from_host);
+#ifdef ENABLE_CUSTOM_CALLS
+		proxy_redirect = true;
+#endif
+	}
 	if (IS_ERR(ret))
 		return send_drop_notify(ctx, src_label, SECLABEL, LXC_ID,
 					ret, CTX_ACT_DROP, METRIC_INGRESS);
@@ -1111,12 +1115,9 @@ int tail_ipv6_policy(struct __ctx_buff *ctx)
 	 * incoming packet (before redirecting, and on the way back from the
 	 * proxy).
 	 */
-	if (ret != CTX_ACT_TX &&
-	    !encode_custom_prog_meta(ctx, ret, src_label)) {
+	if (!proxy_redirect && !encode_custom_prog_meta(ctx, ret, src_label))
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV6_INGRESS);
-		return DROP_MISSED_TAIL_CALL;
-	}
 #endif

 	return ret;
@@ -1127,6 +1128,9 @@ declare_tailcall_if(__and(is_defined(ENABLE_IPV4), is_defined(ENABLE_IPV6)),
 int tail_ipv6_to_endpoint(struct __ctx_buff *ctx)
 {
 	__u32 src_identity = ctx_load_meta(ctx, CB_SRC_LABEL);
+#ifdef ENABLE_CUSTOM_CALLS
+	bool proxy_redirect = false;
+#endif
 	void *data, *data_end;
 	struct ipv6hdr *ip6;
 	__u16 proxy_port = 0;
@@ -1173,19 +1177,26 @@ int tail_ipv6_to_endpoint(struct __ctx_buff *ctx)

 	ret = ipv6_policy(ctx, 0, src_identity, &reason, NULL,
 			  &proxy_port, true);
-	if (ret == POLICY_ACT_PROXY_REDIRECT)
+	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy_hairpin(ctx, proxy_port);
+#ifdef ENABLE_CUSTOM_CALLS
+		proxy_redirect = true;
+#endif
+	}
 out:
 	if (IS_ERR(ret))
 		return send_drop_notify(ctx, src_identity, SECLABEL, LXC_ID,
 					ret, CTX_ACT_DROP, METRIC_INGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, src_identity)) {
+	/* Make sure we skip the tail call when the packet is being redirected
+	 * to a L7 proxy, to avoid running the custom program twice on the
+	 * incoming packet (before redirecting, and on the way back from the
+	 * proxy).
+	 */
+	if (!proxy_redirect && !encode_custom_prog_meta(ctx, ret, src_identity))
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV6_INGRESS);
-		return DROP_MISSED_TAIL_CALL;
-	}
 #endif

 	return ret;
@@ -1384,6 +1395,9 @@ int tail_ipv4_policy(struct __ctx_buff *ctx)
 	int ret, ifindex = ctx_load_meta(ctx, CB_IFINDEX);
 	__u32 src_label = ctx_load_meta(ctx, CB_SRC_LABEL);
 	bool from_host = ctx_load_meta(ctx, CB_FROM_HOST);
+#ifdef ENABLE_CUSTOM_CALLS
+	bool proxy_redirect = false;
+#endif
 	__u16 proxy_port = 0;
 	__u8 reason = 0;

@@ -1392,8 +1406,12 @@ int tail_ipv4_policy(struct __ctx_buff *ctx)

 	ret = ipv4_policy(ctx, ifindex, src_label, &reason, &tuple,
 			  &proxy_port, from_host);
-	if (ret == POLICY_ACT_PROXY_REDIRECT)
+	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy4(ctx, &tuple, proxy_port, from_host);
+#ifdef ENABLE_CUSTOM_CALLS
+		proxy_redirect = true;
+#endif
+	}
 	if (IS_ERR(ret))
 		return send_drop_notify(ctx, src_label, SECLABEL, LXC_ID,
 					ret, CTX_ACT_DROP, METRIC_INGRESS);
@@ -1407,12 +1425,9 @@ int tail_ipv4_policy(struct __ctx_buff *ctx)
 	 * incoming packet (before redirecting, and on the way back from the
 	 * proxy).
 	 */
-	if (ret != CTX_ACT_TX &&
-	    !encode_custom_prog_meta(ctx, ret, src_label)) {
+	if (!proxy_redirect && !encode_custom_prog_meta(ctx, ret, src_label))
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV4_INGRESS);
-		return DROP_MISSED_TAIL_CALL;
-	}
 #endif

 	return ret;
@@ -1423,6 +1438,9 @@ declare_tailcall_if(__and(is_defined(ENABLE_IPV4), is_defined(ENABLE_IPV6)),
 int tail_ipv4_to_endpoint(struct __ctx_buff *ctx)
 {
 	__u32 src_identity = ctx_load_meta(ctx, CB_SRC_LABEL);
+#ifdef ENABLE_CUSTOM_CALLS
+	bool proxy_redirect = false;
+#endif
 	void *data, *data_end;
 	struct iphdr *ip4;
 	__u16 proxy_port = 0;
@@ -1468,19 +1486,26 @@ int tail_ipv4_to_endpoint(struct __ctx_buff *ctx)

 	ret = ipv4_policy(ctx, 0, src_identity, &reason, NULL,
 			  &proxy_port, true);
-	if (ret == POLICY_ACT_PROXY_REDIRECT)
+	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy_hairpin(ctx, proxy_port);
+#ifdef ENABLE_CUSTOM_CALLS
+		proxy_redirect = true;
+#endif
+	}
 out:
 	if (IS_ERR(ret))
 		return send_drop_notify(ctx, src_identity, SECLABEL, LXC_ID,
 					ret, CTX_ACT_DROP, METRIC_INGRESS);

 #ifdef ENABLE_CUSTOM_CALLS
-	if (!encode_custom_prog_meta(ctx, ret, src_identity)) {
+	/* Make sure we skip the tail call when the packet is being redirected
+	 * to a L7 proxy, to avoid running the custom program twice on the
+	 * incoming packet (before redirecting, and on the way back from the
+	 * proxy).
+	 */
+	if (!proxy_redirect && !encode_custom_prog_meta(ctx, ret, src_identity))
 		tail_call_static(ctx, &CUSTOM_CALLS_MAP,
 				 CUSTOM_CALLS_IDX_IPV4_INGRESS);
-		return DROP_MISSED_TAIL_CALL;
-	}
 #endif

 	return ret;
diff --git a/bpf/custom/Makefile b/bpf/custom/Makefile
index fd21f0cbe866..2a250d205a18 100644
--- a/bpf/custom/Makefile
+++ b/bpf/custom/Makefile
@@ -6,6 +6,7 @@ include ../../Makefile.defs
 .PHONY: all clean

 BPF = bpf_custom.o
+BPF_CUSTOM_PROG ?= bytecount.h

 include ../Makefile.bpf

@@ -13,7 +14,7 @@ all: $(BPF)

 %.ll: %.c $(LIB)
 	@$(ECHO_CC)
-	$(QUIET) ${CLANG} ${CLANG_FLAGS} -c $< -o $@
+	$(QUIET) ${CLANG} ${CLANG_FLAGS} -DBPF_CUSTOM_PROG=${BPF_CUSTOM_PROG} -c $< -o $@

 $(BPF): %.o: %.ll
 	@$(ECHO_CC)
diff --git a/bpf/custom/bpf_custom.c b/bpf/custom/bpf_custom.c
index 7600b0ccfc0f..09d574285c44 100644
--- a/bpf/custom/bpf_custom.c
+++ b/bpf/custom/bpf_custom.c
@@ -10,8 +10,14 @@
  * custom_prog(), for example:
  *
  *     #include "bytecount.h"
+ *
+ * You can also pass it to the Makefile:
+ *
+ *     BPF_CUSTOM_PROG=bytecount.h make
  */
-#include "bytecount.h"
+#define TO_STRING(X) #X
+#define STRINGIFY(X) TO_STRING(X)
+#include STRINGIFY(BPF_CUSTOM_PROG)

 __section("custom")
 int custom_hook(const struct __ctx_buff *ctx)
diff --git a/install/kubernetes/cilium/README.md b/install/kubernetes/cilium/README.md
index 9a66a2e0b100..2bca7a538c09 100644
--- a/install/kubernetes/cilium/README.md
+++ b/install/kubernetes/cilium/README.md
@@ -107,8 +107,9 @@ contributors across the globe, there is almost always someone available to help.
 | cni.install | bool | `true` | Install the CNI configuration and binary files into the filesystem. |
 | containerRuntime | object | `{"integration":"none"}` | Configure how frequently garbage collection should occur for the datapath connection tracking table. conntrackGCInterval: "0s" -- Configure container runtime specific integration. |
 | containerRuntime.integration | string | `"none"` | Enables specific integrations for container runtimes. Supported values: - containerd - crio - docker - none - auto (automatically detect the container runtime) |
+| customCalls.enable | bool | `false` | Enable tail call hooks for custom eBPF programs. |
 | daemon.runPath | string | `"/var/run/cilium"` | Configure where Cilium runtime state should be stored. |
-| datapathMode | string | `"veth"` |  |
+| datapathMode | string | `"veth"` | Configure which datapath mode should be used for configuring container connectivity. Valid options are "veth" or "ipvlan". |
 | debug.enabled | bool | `false` | Enable debug logging |
 | enableCnpStatusUpdates | bool | `false` | Specify which network interfaces can run the eBPF datapath. This means that a packet sent from a pod to a destination outside the cluster will be masqueraded (to an output device IPv4 address), if the output device runs the program. When not specified, probing will automatically detect devices. devices: "" TODO: Add documentation disableIptablesFeederRules: "" TODO: Add documentation egressMasqueradeInterfaces: "" |
 | enableCriticalPriorityClass | bool | `true` | Explicitly enable or disable priority class. .Capabilities.KubeVersion is unsettable in `helm template` calls, it depends on k8s libriaries version that Helm was compiled against. This option allows to explicitly disable setting the priority class, which is useful for rendering charts for gke clusters in advance. |
diff --git a/install/kubernetes/cilium/templates/cilium-configmap.yaml b/install/kubernetes/cilium/templates/cilium-configmap.yaml
index 725c6c658212..a0b98510dd97 100644
--- a/install/kubernetes/cilium/templates/cilium-configmap.yaml
+++ b/install/kubernetes/cilium/templates/cilium-configmap.yaml
@@ -680,6 +680,11 @@ data:
   k8s-service-proxy-name: {{ .Values.k8s.serviceProxyName | quote }}
 {{- end }}

+{{- if and .Values.customCalls .Values.customCalls.enabled }}
+  # Enable tail call hooks for custom eBPF programs.
+  enable-custom-calls: {{ .Values.customCalls.enabled | quote }}
+{{- end }}
+
 {{- if .Values.extraConfig }}
 {{ toYaml .Values.extraConfig | indent 2 }}
 {{- end }}
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 670fabee77ef..6686435c97da 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -358,6 +358,11 @@ containerRuntime:

 # crdWaitTimeout: ""

+# -- Tail call hooks for custom eBPF programs.
+customCalls:
+  # -- Enable tail call hooks for custom eBPF programs.
+  enable: false
+
 # -- Configure which datapath mode should be used for configuring container
 # connectivity. Valid options are "veth" or "ipvlan".
 datapathMode: veth

test-me-please

test-me-please

test-me-please

test-me-please

test-me-please

test-me-please

A few changes in this version

I incorporated Joe's suggestion of using a macro for defining the section name in the custom program source.
This broke the section name collection in verifier-test.sh, so there is a new commit to apply some adjustment to that script. This is to avoid parsing the macro name as a section name, and to enable parsing a comment instead (yeah, we cheat a little - middle term the sample will likely move out of Cilium's repo and verifier-test.sh's juridiction anyway).
The test is skipped in GKE (in addition to 4.9), because we don't have the source code around to build the custom sample from on GKE. This will be fixed in the future, by packaging the sample program independently.

Incremental diff, since the previous diff
diff --git a/bpf/custom/Makefile b/bpf/custom/Makefile
index 2a250d205a18..aa18dbf3e12f 100644
--- a/bpf/custom/Makefile
+++ b/bpf/custom/Makefile
@@ -6,7 +6,8 @@ include ../../Makefile.defs
 .PHONY: all clean

 BPF = bpf_custom.o
-BPF_CUSTOM_PROG ?= bytecount.h
+BPF_CUSTOM_PROG_FILE ?= bytecount.h
+BPF_CUSTOM_PROG_NAME ?= custom

 include ../Makefile.bpf

@@ -14,7 +15,10 @@ all: $(BPF)

 %.ll: %.c $(LIB)
 	@$(ECHO_CC)
-	$(QUIET) ${CLANG} ${CLANG_FLAGS} -DBPF_CUSTOM_PROG=${BPF_CUSTOM_PROG} -c $< -o $@
+	$(QUIET) ${CLANG} ${CLANG_FLAGS} \
+		-DBPF_CUSTOM_PROG_FILE=${BPF_CUSTOM_PROG_FILE} \
+		-DBPF_CUSTOM_PROG_NAME=${BPF_CUSTOM_PROG_NAME} \
+		-c $< -o $@

 $(BPF): %.o: %.ll
 	@$(ECHO_CC)
diff --git a/bpf/custom/README.rst b/bpf/custom/README.rst
index d6ac2826a0e9..fe72768102f7 100644
--- a/bpf/custom/README.rst
+++ b/bpf/custom/README.rst
@@ -93,7 +93,7 @@ The following steps indicate how to use this example program.
        # bpftool map dump \
            pinned /sys/fs/bpf/tc/globals/bytecounter_maps/bytecount_map

-5. When the byte counter is no longer neccessary, it is possible to clean it up
+5. When the byte counter is no longer necessary, it is possible to clean it up
    by deleting the entry in the tail call map, and removing the pinned
    objects::

@@ -107,40 +107,19 @@ Using Other Custom Programs
 ===========================

 Other custom programs can be hooked into the datapath, just like the byte
-counter program. The workflow would be similar:
+counter program. The workflow is similar.

-1. Write a custom program, and make sure it does not interfere with the logics
-   of the datapath. It is recommended to define a custom function and to
-   include it in the “landing point” program from bpf_custom.c.
+First, write a custom program, and make sure it does not interfere with the
+logics of the datapath. It is recommended to define a custom function and to
+include it in the “landing point” program from bpf_custom.c.

-2. Compile your program. If reusing bpf_custom.c, this is easy to do. Just pass
-   the name of the file containing the ``custom_prog`` function when calling
-   make::
+Then, compile your program. If reusing bpf_custom.c, this is easy to do. Just
+pass the name of the file containing the ``custom_prog`` function when calling
+make::

-       $ BPF_CUSTOM_PROG=custom_function.h make -C bpf/custom
+       $ BPF_CUSTOM_PROG_FILE=custom_function.h make -C bpf/custom

-3. Load and pin the custom program::
-
-       # bpftool prog load bpf/custom/bpf_custom.o \
-           /sys/fs/bpf/tc/globals/custom_prog \
-           type classifier
-
-4. Select your endpoint and update its custom tail call map with a reference to
-   the custom program, for the desired hook (direction and IP version). The
-   command below is for ingress on IPv4, for the endpoint with id 1234::
-
-       # bpftool map update \
-           pinned /sys/fs/bpf/tc/globals/cilium_calls_custom_01234 \
-           key 0 0 0 0 \
-           value pinned /sys/fs/bpf/tc/globals/custom_prog
-
-5. The program should run for each packet. If it uses maps to share data with
-   userspace, it should now be possible to retrieve the statistics.
-
-6. Clean up by deleting the entry in the tail call map and removing all pinned
-   objects in the eBPF virtual file system::
-
-       # bpftool map delete \
-           pinned /sys/fs/bpf/tc/globals/cilium_calls_custom_01234 \
-           key 0 0 0 0
-       # rm /sys/fs/bpf/tc/globals/custom_prog
+The environment variable ``BPF_CUSTOM_PROG_NAME`` is similarly available to set
+the name of the ELF section where the resulting program will be located. This
+is often used to set either the program name, or, depending on the loading
+method, to indicate the type of the program to the loader.
diff --git a/bpf/custom/bpf_custom.c b/bpf/custom/bpf_custom.c
index 09d574285c44..5c2338c0476d 100644
--- a/bpf/custom/bpf_custom.c
+++ b/bpf/custom/bpf_custom.c
@@ -6,20 +6,29 @@

 #include "lib/common.h"

-/* Include custom header file (.h) containing the implementation for
- * custom_prog(), for example:
- *
- *     #include "bytecount.h"
- *
- * You can also pass it to the Makefile:
- *
- *     BPF_CUSTOM_PROG=bytecount.h make
- */
 #define TO_STRING(X) #X
 #define STRINGIFY(X) TO_STRING(X)
-#include STRINGIFY(BPF_CUSTOM_PROG)

-__section("custom")
+/* Use the macros below to set the name of the program and the name of the file
+ * containing the implementation for custom_prog(). The values for these macros
+ * should typically be passed to the Makefile, for example:
+ *
+ *     BPF_CUSTOM_PROG_FILE=bytecount.h make
+ */
+
+#ifndef BPF_CUSTOM_PROG_FILE
+/* Default to bytecount.h for the included file */
+#define BPF_CUSTOM_PROG_FILE bytecount.h
+#endif
+
+#ifndef BPF_CUSTOM_PROG_NAME
+/* Default to __section("custom") for the program */
+#define BPF_CUSTOM_PROG_NAME custom
+#endif
+
+#include STRINGIFY(BPF_CUSTOM_PROG_FILE)
+
+__section(STRINGIFY(BPF_CUSTOM_PROG_NAME))
 int custom_hook(const struct __ctx_buff *ctx)
 {
 	__u32 custom_meta = ctx_load_meta(ctx, CB_CUSTOM_CALLS);
diff --git a/test/bpf/verifier-test.sh b/test/bpf/verifier-test.sh
index df701b789a1d..209bc8667459 100755
--- a/test/bpf/verifier-test.sh
+++ b/test/bpf/verifier-test.sh
@@ -61,7 +61,7 @@ function cleanup {
 }

 function get_section {
-	grep "__section(" $DIR/$1 | sed 's/__sec[^\"]*\"\([0-9A-Za-z_-]*\).*/\1/'
+	sed -n '/__section("/ s/.*__section("\([0-9A-Za-z_-]*\).*/\1/p' $DIR/$1
 }

 function load_prog {
diff --git a/test/k8sT/CustomCalls.go b/test/k8sT/CustomCalls.go
index 2126e98de5dd..43ed7af1cb47 100644
--- a/test/k8sT/CustomCalls.go
+++ b/test/k8sT/CustomCalls.go
@@ -53,7 +53,6 @@ var _ = Describe("K8sCustomCalls", func() {

 	AfterEach(func() {
 		deploymentManager.DeleteAll()
-		// OR ExpectAllPodsTerminated(kubectl)
 	})

 	AfterFailed(func() {
@@ -62,7 +61,6 @@ var _ = Describe("K8sCustomCalls", func() {

 	AfterAll(func() {
 		deploymentManager.DeleteCilium()
-		// OR UninstallCiliumFromManifest(kubectl, ciliumFilename)
 		kubectl.CloseSSHClient()
 	})

@@ -75,8 +73,13 @@ var _ = Describe("K8sCustomCalls", func() {
 		// Believe it or not, bpftool internally attempts to retrieve
 		// map info before updating a map, but BPF_OBJ_GET_INFO_BY_FD
 		// is not supported on kernel 4.9 (it was introduced in Linux
-		// 4.13).
-		return helpers.DoesNotRunOnNetNextOr419Kernel()
+		// 4.13). Skip on 4.9 kernels.
+		//
+		// Also skip on GKE because we do not have the source of the
+		// custom program available on a node, for the compiler pod to
+		// pick up.
+		return helpers.DoesNotRunOnNetNextOr419Kernel() ||
+			helpers.RunsOnGKE()
 	}, "Basic test with byte counter", func() {

 		var (
@@ -93,6 +96,8 @@ var _ = Describe("K8sCustomCalls", func() {
 			mapPinPath  string
 		)

+		const compilerPodName string = "bytecounter-compiler"
+
 		BeforeAll(func() {
 			// Initialize all paths. This cannot be done at
 			// variable declaration because kubectl is not set when
@@ -108,12 +113,20 @@ var _ = Describe("K8sCustomCalls", func() {
 			yaml = helpers.ManifestGet(kubectl.BasePath(), "demo-customcalls.yaml")
 			kubectl.ApplyDefault(yaml).ExpectSuccess("Unable to apply %s", yaml)

-			By("Checking the availability of the custom program to load")
+			By("Compiling custom byte counter program")

-			cmd := fmt.Sprintf("stat --format %%F %s", localObjFile)
-			res := kubectl.Exec(cmd)
-			res.ExpectSuccess(fmt.Sprintf("Could not find object file %s for custom byte counter program. Did you run 'make -C bpf' from the root of Cilium's repository?", localObjFile))
-			Expect(strings.TrimSpace(res.Stdout())).To(Equal("regular file"), fmt.Sprintf("File %s is not a regular file", localObjFile))
+			err := kubectl.WaitForSinglePod(helpers.DefaultNamespace, compilerPodName, helpers.HelperTimeout)
+			Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s pod not ready after timeout", compilerPodName))
+
+			const bpfCustomDir string = "bpf/custom"
+
+			cmd := fmt.Sprintf("make -C %s clean V=0", bpfCustomDir)
+			res := kubectl.ExecPodCmd(helpers.DefaultNamespace, compilerPodName, cmd)
+			res.ExpectSuccess(fmt.Sprintf("Failed to clean up %s directory", bpfCustomDir))
+
+			cmd = fmt.Sprintf("make -C %s V=0", bpfCustomDir)
+			res = kubectl.ExecPodCmd(helpers.DefaultNamespace, compilerPodName, cmd)
+			res.ExpectSuccess("Failed to build custom byte counter program")
 		})

 		AfterAll(func() {
@@ -152,17 +165,27 @@ var _ = Describe("K8sCustomCalls", func() {

 			By("Retrieving counter value")

-			cmd = fmt.Sprintf("bpftool -j map lookup pinned %s key %s", mapPinPath, serverIdentity)
-			cmd += " | jq -r '.value[]' | awk -n 'BEGIN {i=0; s=0} {s+=($0*(2**i)); i+=8} END {print s}'"
+			cmd = fmt.Sprintf("bpftool -j map lookup pinned %s key %s | jq -r '.value[]'", mapPinPath, serverIdentity)
 			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
 			res.ExpectSuccess(fmt.Sprintf("Failed to lookup byte counter value ('%s'): %s", cmd, res.Stderr()))
-			count, err := strconv.Atoi(strings.TrimSpace(res.Stdout()))
-			Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to convert byte counter value to an integer (%s)", err))
+
+			// Output from bpftool is on the form
+			// "0x11\n0x22\n0x33\n...0x88\n", convert it.
+			// Too bad bpftool returns hex that jq doesn't know how
+			// to convert yet.
+			var count uint64 = 0
+			byteString := strings.TrimSpace(res.Stdout())
+			bytes := strings.Split(byteString, "\n")
+			for i, b := range bytes {
+				dec, err := strconv.ParseUint(b, 0, 8)
+				Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to convert byte counter value to an integer (%s from %q): %s", b, bytes, err))
+				count += dec << (i * 8)
+			}

 			By("Checking counter value")

 			expectedCount := 98 * helpers.PingCount
-			Expect(count).To(Equal(expectedCount), fmt.Sprintf("Byte count (%d) differs from expected value (%d)", count, expectedCount))
+			Expect(int(count)).To(Equal(expectedCount), fmt.Sprintf("Byte count (%d) differs from expected value (%d)", count, expectedCount))
 		}

 		cleanupByteCounter := func(endpointId int64, ciliumPod string, serverIdentity string, direction customCallDirection) {
diff --git a/test/k8sT/manifests/demo-customcalls.yaml b/test/k8sT/manifests/demo-customcalls.yaml
index 29909bad3673..502b0f34d0e8 100644
--- a/test/k8sT/manifests/demo-customcalls.yaml
+++ b/test/k8sT/manifests/demo-customcalls.yaml
@@ -91,3 +91,46 @@ spec:
       # call hook for socket-based load-balancing is not supported yet.
       nodeSelector:
         "cilium.io/ci-node": k8s2
+---
+apiVersion: v1
+kind: Pod
+metadata:
+  name: bytecounter-compiler
+spec:
+  containers:
+  - name: cilium-builder
+    image: quay.io/cilium/cilium-builder:6be5825403f3c7ec655914e3816a1978dd253119@sha256:edca1b5fd14f6a2f5e24e93ae377e3423d946c5d5769a06ee7bf242b5850ef20
+    workingDir: /cilium
+    command: ["sleep"]
+    args:
+      - "1000h"
+    securityContext:
+      capabilities:
+        add:
+        - NET_ADMIN
+        - SYS_MODULE
+      privileged: true
+    volumeMounts:
+      - mountPath: /sys/fs/bpf
+        name: bpf-maps
+      - mountPath: /cilium
+        name: cilium-src
+  volumes:
+  - hostPath:
+      path: /sys/fs/bpf
+      type: DirectoryOrCreate
+    name: bpf-maps
+  - hostPath:
+      path: /home/vagrant/go/src/github.com/cilium/cilium
+      type: Directory
+    name: cilium-src
+  # We need the following toleration overwrite because the pod is used with
+  # Cilium uninstalled, so the network isn't ready.
+  tolerations:
+  - key: "node.kubernetes.io/not-ready"
+    operator: "Exists"
+  - key: "node.kubernetes.io/unreachable"
+    operator: "Exists"
+  hostNetwork: true
+  nodeSelector:
+    "cilium.io/ci-node": k8s2

test-me-please

test-1.20.4-9

test-1.20-4.9

test-1.20-4.9

Latest version polishes the CI test a little, and should address most of the issues raised by Paul. Not included but left as follow-ups:

The addition of a new metrics for skipped/failed tail calls
Bugtool and logging extension in case of CI failure to include e.g. custom call map info

Note the new It() block in the CI test, to test the byte-counter with per-endpoint routes.
Incremental diff
diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c
index d2dcf56a6355..88957709c80d 100644
--- a/bpf/bpf_lxc.c
+++ b/bpf/bpf_lxc.c
@@ -1085,9 +1085,7 @@ int tail_ipv6_policy(struct __ctx_buff *ctx)
 	int ret, ifindex = ctx_load_meta(ctx, CB_IFINDEX);
 	__u32 src_label = ctx_load_meta(ctx, CB_SRC_LABEL);
 	bool from_host = ctx_load_meta(ctx, CB_FROM_HOST);
-#ifdef ENABLE_CUSTOM_CALLS
-	bool proxy_redirect = false;
-#endif
+	bool proxy_redirect __maybe_unused = false;
 	__u16 proxy_port = 0;
 	__u8 reason = 0;

@@ -1098,9 +1096,7 @@ int tail_ipv6_policy(struct __ctx_buff *ctx)
 			  &proxy_port, from_host);
 	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy6(ctx, &tuple, proxy_port, from_host);
-#ifdef ENABLE_CUSTOM_CALLS
 		proxy_redirect = true;
-#endif
 	}
 	if (IS_ERR(ret))
 		return send_drop_notify(ctx, src_label, SECLABEL, LXC_ID,
@@ -1128,9 +1124,7 @@ declare_tailcall_if(__and(is_defined(ENABLE_IPV4), is_defined(ENABLE_IPV6)),
 int tail_ipv6_to_endpoint(struct __ctx_buff *ctx)
 {
 	__u32 src_identity = ctx_load_meta(ctx, CB_SRC_LABEL);
-#ifdef ENABLE_CUSTOM_CALLS
-	bool proxy_redirect = false;
-#endif
+	bool proxy_redirect __maybe_unused = false;
 	void *data, *data_end;
 	struct ipv6hdr *ip6;
 	__u16 proxy_port = 0;
@@ -1179,9 +1173,7 @@ int tail_ipv6_to_endpoint(struct __ctx_buff *ctx)
 			  &proxy_port, true);
 	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy_hairpin(ctx, proxy_port);
-#ifdef ENABLE_CUSTOM_CALLS
 		proxy_redirect = true;
-#endif
 	}
 out:
 	if (IS_ERR(ret))
@@ -1395,9 +1387,7 @@ int tail_ipv4_policy(struct __ctx_buff *ctx)
 	int ret, ifindex = ctx_load_meta(ctx, CB_IFINDEX);
 	__u32 src_label = ctx_load_meta(ctx, CB_SRC_LABEL);
 	bool from_host = ctx_load_meta(ctx, CB_FROM_HOST);
-#ifdef ENABLE_CUSTOM_CALLS
-	bool proxy_redirect = false;
-#endif
+	bool proxy_redirect __maybe_unused = false;
 	__u16 proxy_port = 0;
 	__u8 reason = 0;

@@ -1408,9 +1398,7 @@ int tail_ipv4_policy(struct __ctx_buff *ctx)
 			  &proxy_port, from_host);
 	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy4(ctx, &tuple, proxy_port, from_host);
-#ifdef ENABLE_CUSTOM_CALLS
 		proxy_redirect = true;
-#endif
 	}
 	if (IS_ERR(ret))
 		return send_drop_notify(ctx, src_label, SECLABEL, LXC_ID,
@@ -1438,9 +1426,7 @@ declare_tailcall_if(__and(is_defined(ENABLE_IPV4), is_defined(ENABLE_IPV6)),
 int tail_ipv4_to_endpoint(struct __ctx_buff *ctx)
 {
 	__u32 src_identity = ctx_load_meta(ctx, CB_SRC_LABEL);
-#ifdef ENABLE_CUSTOM_CALLS
-	bool proxy_redirect = false;
-#endif
+	bool proxy_redirect __maybe_unused = false;
 	void *data, *data_end;
 	struct iphdr *ip4;
 	__u16 proxy_port = 0;
@@ -1488,9 +1474,7 @@ int tail_ipv4_to_endpoint(struct __ctx_buff *ctx)
 			  &proxy_port, true);
 	if (ret == POLICY_ACT_PROXY_REDIRECT) {
 		ret = ctx_redirect_to_proxy_hairpin(ctx, proxy_port);
-#ifdef ENABLE_CUSTOM_CALLS
 		proxy_redirect = true;
-#endif
 	}
 out:
 	if (IS_ERR(ret))
diff --git a/bpf/custom/README.rst b/bpf/custom/README.rst
index fe72768102f7..6d5cae9df163 100644
--- a/bpf/custom/README.rst
+++ b/bpf/custom/README.rst
@@ -4,12 +4,11 @@
 Tail Call Hooks for Custom Programs
 ===================================

-The files in this directory implement a sample byte counter program which can
+The files in this directory implement a sample byte-counter program which can
 be attached to dedicated hooks in the datapath.

-.. note::
-    This is a beta feature. Please provide feedback and file a GitHub issue if
-    you experience any problems.
+Note: This is a beta feature. Please provide feedback and file a GitHub issue
+if you experience any problems.

 Datapath Hooks
 ==============
@@ -18,10 +17,10 @@ Implementation
 --------------

 When compiled with ``ENABLE_CUSTOM_CALLS`` set, Cilium's datapath contains some
-tail call hooks permitting to insert custom programs in the datapath. These
-custom programs should be particularly careful *not* to interfere with the
-logics of the datapath, at the risk of altering or breaking the network
-connectivity in the cluster.
+tail call hooks to insert custom programs in the datapath. These custom
+programs should be particularly careful *not* to interfere with the logics of
+the datapath, at the risk of altering or breaking the network connectivity in
+the cluster.

 Before the tail call happens, the datapath encodes two elements into the ``cb``
 field of the socket buffer for the packet:
@@ -32,24 +31,24 @@ field of the socket buffer for the packet:
   custom program must be called as the very last step of the processing of the
   packet, and must preserve the return value selected by the datapath.

-- The identity corresponding to the packet, that is, the Cilium identity
-  associated to the sender for the ingress hook, and to the receiver when on
-  the egress hook. This identity is not strictly necessary to the custom
-  program, but it prove useful for some use cases. Given that the datapath
-  already did the work for retrieving the identity for the packet, it just
-  passes it down to the custom programs, in case it saves them the trouble to
-  extract it a second time.
+- The remote security identity associated with the packet, that is, the Cilium
+  identity associated with the sender for the ingress hook, and with the
+  receiver when on the egress hook. This identity is not strictly necessary to
+  the custom program, but it proved useful for some use cases. Given that the
+  datapath already did the work for retrieving the identity for the packet, it
+  just passes it down to the custom programs, in case it saves them the trouble
+  to extract it a second time.

 Limitations
 -----------

-- There is currently no hook on the egress path for socket-based
+- There is currently no hook on the egress path for socket-level
   load-balancing.

-Sample Byte Counter Program
+Sample Byte-Counter Program
 ============================

-This directory contains a sample byte counter program, to help count how many
+This directory contains a sample byte-counter program, to help count how many
 bytes a given endpoint sends or receives, and to understand how the tail call
 hooks work.

@@ -60,7 +59,7 @@ The following steps indicate how to use this example program.

        $ make -C bpf/custom

-2. Load and pin the byte counter program. Bpftool can do it::
+2. Load and pin the byte-counter program. Bpftool can do it::

        # bpftool prog load bpf/custom/bpf_custom.o \
            /sys/fs/bpf/tc/globals/bytecounter \
@@ -69,7 +68,7 @@ The following steps indicate how to use this example program.

 3. Pick the endpoint for which the program should count the bytes. Update the
    tail call map for custom programs for that endpoint, with a reference to the
-   pinned byte counter program::
+   pinned byte-counter program::

        $ EP=<endpoint_id>
        # bpftool map update \
@@ -87,13 +86,13 @@ The following steps indicate how to use this example program.
    - IPv6, ingress: ``key 2 0 0 0``
    - IPv6, egress:  ``key 3 0 0 0``

-4. After some traffic has flown, dump the content of the byte counter map to
+4. After some traffic has flown, dump the content of the byte-counter map to
    read the statistics::

        # bpftool map dump \
            pinned /sys/fs/bpf/tc/globals/bytecounter_maps/bytecount_map

-5. When the byte counter is no longer necessary, it is possible to clean it up
+5. When the byte-counter is no longer necessary, it is possible to clean it up
    by deleting the entry in the tail call map, and removing the pinned
    objects::

diff --git a/install/kubernetes/cilium/README.md b/install/kubernetes/cilium/README.md
index 2bca7a538c09..c5205975fca7 100644
--- a/install/kubernetes/cilium/README.md
+++ b/install/kubernetes/cilium/README.md
@@ -107,7 +107,7 @@ contributors across the globe, there is almost always someone available to help.
 | cni.install | bool | `true` | Install the CNI configuration and binary files into the filesystem. |
 | containerRuntime | object | `{"integration":"none"}` | Configure how frequently garbage collection should occur for the datapath connection tracking table. conntrackGCInterval: "0s" -- Configure container runtime specific integration. |
 | containerRuntime.integration | string | `"none"` | Enables specific integrations for container runtimes. Supported values: - containerd - crio - docker - none - auto (automatically detect the container runtime) |
-| customCalls.enable | bool | `false` | Enable tail call hooks for custom eBPF programs. |
+| customCalls.enabled | bool | `false` | Enable tail call hooks for custom eBPF programs. |
 | daemon.runPath | string | `"/var/run/cilium"` | Configure where Cilium runtime state should be stored. |
 | datapathMode | string | `"veth"` | Configure which datapath mode should be used for configuring container connectivity. Valid options are "veth" or "ipvlan". |
 | debug.enabled | bool | `false` | Enable debug logging |
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index 6686435c97da..277140e66f18 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -361,7 +361,7 @@ containerRuntime:
 # -- Tail call hooks for custom eBPF programs.
 customCalls:
   # -- Enable tail call hooks for custom eBPF programs.
-  enable: false
+  enabled: false

 # -- Configure which datapath mode should be used for configuring container
 # connectivity. Valid options are "veth" or "ipvlan".
diff --git a/test/bpf/verifier-test.sh b/test/bpf/verifier-test.sh
index 209bc8667459..a181aa5b4c7b 100755
--- a/test/bpf/verifier-test.sh
+++ b/test/bpf/verifier-test.sh
@@ -61,7 +61,7 @@ function cleanup {
 }

 function get_section {
-	sed -n '/__section("/ s/.*__section("\([0-9A-Za-z_-]*\).*/\1/p' $DIR/$1
+	sed -n 's/.*__section("\([0-9A-Za-z_-]*\).*/\1/p' $DIR/$1
 }

 function load_prog {
diff --git a/test/k8sT/CustomCalls.go b/test/k8sT/CustomCalls.go
index 43ed7af1cb47..1014eac8afa6 100644
--- a/test/k8sT/CustomCalls.go
+++ b/test/k8sT/CustomCalls.go
@@ -75,12 +75,16 @@ var _ = Describe("K8sCustomCalls", func() {
 		// is not supported on kernel 4.9 (it was introduced in Linux
 		// 4.13). Skip on 4.9 kernels.
 		//
+		// This leaves us with 4.19 and net-next. Coverage should be
+		// identical on the two versions, so just run on net-next.
+		//
 		// Also skip on GKE because we do not have the source of the
 		// custom program available on a node, for the compiler pod to
-		// pick up.
-		return helpers.DoesNotRunOnNetNextOr419Kernel() ||
+		// pick up (although technically, skipping 4.19 kernels already
+		// skips GKE).
+		return helpers.DoesNotRunOnNetNextKernel() ||
 			helpers.RunsOnGKE()
-	}, "Basic test with byte counter", func() {
+	}, "Basic test with byte-counter", func() {

 		var (
 			yaml string
@@ -94,9 +98,17 @@ var _ = Describe("K8sCustomCalls", func() {
 			progPinPath string
 			mapsPinDir  string
 			mapPinPath  string
+
+			// Pods from the manifest
+			podList v1.PodList
+			podApp1 v1.Pod
+			podApp2 v1.Pod
 		)

-		const compilerPodName string = "bytecounter-compiler"
+		const (
+			compilerPodName string = "bytecounter-compiler"
+			pingBytes       uint   = 98 * helpers.PingCount
+		)

 		BeforeAll(func() {
 			// Initialize all paths. This cannot be done at
@@ -113,7 +125,7 @@ var _ = Describe("K8sCustomCalls", func() {
 			yaml = helpers.ManifestGet(kubectl.BasePath(), "demo-customcalls.yaml")
 			kubectl.ApplyDefault(yaml).ExpectSuccess("Unable to apply %s", yaml)

-			By("Compiling custom byte counter program")
+			By("Compiling custom byte-counter program")

 			err := kubectl.WaitForSinglePod(helpers.DefaultNamespace, compilerPodName, helpers.HelperTimeout)
 			Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("%s pod not ready after timeout", compilerPodName))
@@ -126,7 +138,7 @@ var _ = Describe("K8sCustomCalls", func() {

 			cmd = fmt.Sprintf("make -C %s V=0", bpfCustomDir)
 			res = kubectl.ExecPodCmd(helpers.DefaultNamespace, compilerPodName, cmd)
-			res.ExpectSuccess("Failed to build custom byte counter program")
+			res.ExpectSuccess("Failed to build custom byte-counter program")
 		})

 		AfterAll(func() {
@@ -134,26 +146,70 @@ var _ = Describe("K8sCustomCalls", func() {
 			ExpectAllPodsTerminated(kubectl)
 		})

+		getPodsInfo := func() {
+			By("Retrieving pods information")
+			// Get pods app1 (HTTP/FTP server) on node 1, and app2
+			// (client) on node 2
+			err := kubectl.GetPods(helpers.DefaultNamespace, "-l id=app1").Unmarshal(&podList)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(len(podList.Items)).To(Equal(1))
+			podApp1 = podList.Items[0]
+
+			err = kubectl.GetPods(helpers.DefaultNamespace, "-l id=app2").Unmarshal(&podList)
+			Expect(err).ToNot(HaveOccurred())
+			Expect(len(podList.Items)).To(Equal(1))
+			podApp2 = podList.Items[0]
+		}
+
+		copyAndLoadObjectFile := func(ciliumPod string) {
+			By("Copying custom byte-counter program to Cilium pod")
+
+			cmd := fmt.Sprintf("kubectl -n kube-system cp %s %s:%s", localObjFile, ciliumPod, remoteObjFile)
+			res := kubectl.Exec(cmd)
+			res.ExpectSuccess(fmt.Sprintf("Failed to copy custom program from %s to %s:%s: %s",
+				localObjFile, ciliumPod, remoteObjFile, res.Stderr()))
+
+			By("Loading custom byte-counter program")
+
+			cmd = fmt.Sprintf("bpftool prog load %s %s type classifier pinmaps %s", remoteObjFile, progPinPath, mapsPinDir)
+			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
+			res.ExpectSuccess(fmt.Sprintf("Failed to load custom program ('%s'): %s", cmd, res.Stderr()))
+		}
+
+		getIdentityKey := func(label string, ciliumPod string) string {
+			cmd := fmt.Sprintf("cilium identity list -o json | jq '.[]|select(.labels[]|contains(\"%s\")).id'", label)
+			res := kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
+			res.ExpectSuccess(fmt.Sprintf("Failed to retrieve pod identity ('%s'): %s", cmd, res.Stderr()))
+			identity, err := strconv.Atoi(strings.TrimSpace(res.Stdout()))
+			ExpectWithOffset(1, err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to convert pod identity to an integer (%s)", err))
+			return fmt.Sprintf("%d %d %d %d",
+				identity&0xff,
+				(identity>>8)&0xff,
+				(identity>>16)&0xff,
+				(identity>>24)&0xff)
+		}
+
+		extractCounterValue := func(output string) uint {
+			var count uint64 = 0
+			byteString := strings.TrimSpace(output)
+			bytes := strings.Split(byteString, "\n")
+			for i, b := range bytes {
+				dec, err := strconv.ParseUint(b, 0, 8)
+				ExpectWithOffset(2, err).ToNot(HaveOccurred(),
+					fmt.Sprintf("Failed to convert byte-counter value to an integer (%s from %q): %s", b, bytes, err))
+				count += dec << (i * 8)
+			}
+			return uint(count)
+		}
+
 		checkOneDirection := func(endpointId int64, ciliumPod string,
 			clientName string, serverIP string, serverIdentity string,
-			direction customCallDirection) {
-
-			switch direction {
-			case IngressIPv4:
-				break
-			case EgressIPv4:
-				break
-			default:
-				// IPv6 packets do not have the same length,
-				// and the comparison on the counter value will
-				// fail. Could be adjusted in the future.
-				Failf("Direction not supported: %v", direction)
-			}
+			direction customCallDirection, expectedCount uint) {

 			By("Updating call map with reference to custom program")

-			// TODO: other directions
-			cmd := fmt.Sprintf("bpftool map update pinned %scilium_calls_custom_%05d key %d 0 0 0 value pinned %s", bpffsDir, endpointId, direction, progPinPath)
+			cmd := fmt.Sprintf("bpftool map update pinned %scilium_calls_custom_%05d key %d 0 0 0 value pinned %s",
+				bpffsDir, endpointId, direction, progPinPath)
 			res := kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
 			res.ExpectSuccess(fmt.Sprintf("Failed to update call map with reference to custom program ('%s'): %s", cmd, res.Stderr()))

@@ -161,50 +217,68 @@ var _ = Describe("K8sCustomCalls", func() {

 			cmd = helpers.Ping(serverIP)
 			res = kubectl.ExecPodCmd(helpers.DefaultNamespace, clientName, cmd)
-			res.ExpectSuccess(fmt.Sprintf("Failed to ping from %s to %s", "", ""))
+			res.ExpectSuccess(fmt.Sprintf("Failed to ping from %s to %s", clientName, serverIP))

 			By("Retrieving counter value")

 			cmd = fmt.Sprintf("bpftool -j map lookup pinned %s key %s | jq -r '.value[]'", mapPinPath, serverIdentity)
 			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
-			res.ExpectSuccess(fmt.Sprintf("Failed to lookup byte counter value ('%s'): %s", cmd, res.Stderr()))
+			res.ExpectSuccess(fmt.Sprintf("Failed to lookup byte-counter value ('%s'): %s", cmd, res.Stderr()))

 			// Output from bpftool is on the form
 			// "0x11\n0x22\n0x33\n...0x88\n", convert it.
 			// Too bad bpftool returns hex that jq doesn't know how
 			// to convert yet.
-			var count uint64 = 0
-			byteString := strings.TrimSpace(res.Stdout())
-			bytes := strings.Split(byteString, "\n")
-			for i, b := range bytes {
-				dec, err := strconv.ParseUint(b, 0, 8)
-				Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to convert byte counter value to an integer (%s from %q): %s", b, bytes, err))
-				count += dec << (i * 8)
-			}
+			count := extractCounterValue(res.Stdout())

 			By("Checking counter value")

-			expectedCount := 98 * helpers.PingCount
-			Expect(int(count)).To(Equal(expectedCount), fmt.Sprintf("Byte count (%d) differs from expected value (%d)", count, expectedCount))
+			ExpectWithOffset(1, count).To(Equal(expectedCount),
+				fmt.Sprintf("Byte count (%d) differs from expected value (%d)", count, expectedCount))
 		}

-		cleanupByteCounter := func(endpointId int64, ciliumPod string, serverIdentity string, direction customCallDirection) {
+		cleanupByteCounter := func(endpointId int64, ciliumPod string,
+			serverIdentity string, direction customCallDirection) {
+
 			// Clean up tail call map entry
 			cmd := fmt.Sprintf("bpftool map delete pinned %scilium_calls_custom_%05d key %d 0 0 0", bpffsDir, endpointId, direction)
 			res := kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
 			if res.GetExitCode() != 0 {
-				log.Warningf("Failed to remove reference to byte counter program from tail call map ('%s'): %s", cmd, res.Stderr())
+				log.Warningf("Failed to remove reference to byte-counter program from tail call map ('%s'): %s", cmd, res.Stderr())
 			}

-			// Reset byte counter entry
+			// Reset byte-counter entry
 			cmd = fmt.Sprintf("bpftool map delete pinned %s key %s", mapPinPath, serverIdentity)
 			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
 			if res.GetExitCode() != 0 {
-				log.Warningf("Failed to delete entry from byte counter map ('%s'): %s", cmd, res.Stderr())
+				log.Warningf("Failed to delete entry from byte-counter map ('%s'): %s", cmd, res.Stderr())
 			}
 		}

-		It("Loads byte counter and get consistent values", func() {
+		cleanupLoadedObjects := func(ciliumPod string) {
+			By("Cleaning up pinned artefacts")
+
+			// Clean up custom program
+			cmd := "rm -- " + progPinPath
+			res := kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
+			if res.GetExitCode() != 0 {
+				log.Warningf("Failed to unpin byte-counter program ('%s'): %s", cmd, res.Stderr())
+			}
+
+			// Clean up map for custom program
+			cmd = "rm -- " + mapPinPath
+			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
+			if res.GetExitCode() != 0 {
+				log.Warningf("Failed to unpin byte-counter map ('%s'): %s", cmd, res.Stderr())
+			}
+			cmd = "rmdir -- " + mapsPinDir
+			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPod, cmd)
+			if res.GetExitCode() != 0 {
+				log.Warningf("Failed to remove directory for byte-counter pinned map ('%s'): %s", cmd, res.Stderr())
+			}
+		}
+
+		It("Loads byte-counter and gets consistent values", func() {

 			// Deploy Cilium, enable tail calls to custom programs
 			deploymentManager.DeployCilium(map[string]string{
@@ -213,24 +287,12 @@ var _ = Describe("K8sCustomCalls", func() {

 			// Get Cilium pod on node 2
 			ciliumPodK8s2, err := kubectl.GetCiliumPodOnNode(helpers.K8s2)
-			ExpectWithOffset(1, err).ShouldNot(HaveOccurred(), "Cannot get cilium pod on k8s1")
+			Expect(err).ShouldNot(HaveOccurred(), "Cannot get cilium pod on k8s2")

-			// Get pods app1 (HTTP/FTP server) on node 1, and app2
-			// (client), on node 2
-			var (
-				podList v1.PodList
-				podApp1 v1.Pod
-				podApp2 v1.Pod
-			)
-			err = kubectl.GetPods(helpers.DefaultNamespace, "-l id=app1").Unmarshal(&podList)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(len(podList.Items)).To(Equal(1))
-			podApp1 = podList.Items[0]
+			copyAndLoadObjectFile(ciliumPodK8s2)
+			defer cleanupLoadedObjects(ciliumPodK8s2)

-			err = kubectl.GetPods(helpers.DefaultNamespace, "-l id=app2").Unmarshal(&podList)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(len(podList.Items)).To(Equal(1))
-			podApp2 = podList.Items[0]
+			getPodsInfo()

 			// Get ID for the endpoint for which we count the bytes
 			endpoint, err := kubectl.GetCiliumEndpoint(helpers.DefaultNamespace, podApp2.Name)
@@ -238,69 +300,63 @@ var _ = Describe("K8sCustomCalls", func() {
 			Expect(endpoint).ShouldNot(BeNil(), fmt.Sprintf("Retrieved empty endpoint id for pod %s", podApp2.Name))
 			endpointId := endpoint.ID

-			// Wait for pods to be ready
-			err = kubectl.WaitforPods(helpers.DefaultNamespace, "-l zgroup=testapp", helpers.HelperTimeout)
-			Expect(err).Should(BeNil())
-
-			By("Copying custom byte counter program to Cilium pod")
-
-			cmd := fmt.Sprintf("kubectl -n kube-system cp %s %s:%s", localObjFile, ciliumPodK8s2, remoteObjFile)
-			res := kubectl.Exec(cmd)
-			res.ExpectSuccess(fmt.Sprintf("Failed to copy custom program from %s to %s:%s: %s", localObjFile, ciliumPodK8s2, remoteObjFile, res.Stderr()))
-
-			By("Loading custom byte counter program")
-
-			cmd = fmt.Sprintf("bpftool prog load %s %s type classifier pinmaps %s", remoteObjFile, progPinPath, mapsPinDir)
-			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPodK8s2, cmd)
-			res.ExpectSuccess(fmt.Sprintf("Failed to load custom program ('%s'): %s", cmd, res.Stderr()))
-
-			cleanupLoadedObjects := func() {
-				By("Cleaning up pinned artefacts")
-
-				// Clean up custom program
-				cmd = "rm -- " + progPinPath
-				res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPodK8s2, cmd)
-				if res.GetExitCode() != 0 {
-					log.Warningf("Failed to unpin byte counter program ('%s'): %s", cmd, res.Stderr())
-				}
-
-				// Clean up map for custom program
-				cmd = "rm -- " + mapPinPath
-				res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPodK8s2, cmd)
-				if res.GetExitCode() != 0 {
-					log.Warningf("Failed to unpin byte counter map ('%s'): %s", cmd, res.Stderr())
-				}
-				cmd = "rmdir -- " + mapsPinDir
-				res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPodK8s2, cmd)
-				if res.GetExitCode() != 0 {
-					log.Warningf("Failed to remove directory for byte counter pinned map ('%s'): %s", cmd, res.Stderr())
-				}
-			}
-			defer cleanupLoadedObjects()
-
 			// Get the identity of the pod with which the monitored
 			// pod communicates. This identity is used as a key in
-			// the byte counter hash map.
-			cmd = "cilium identity list -o json | jq '.[]|select(.labels[]|contains(\"k8s:id=app1\")).id'"
-			res = kubectl.ExecPodCmd(helpers.KubeSystemNamespace, ciliumPodK8s2, cmd)
-			res.ExpectSuccess(fmt.Sprintf("Failed to retrieve pod identity ('%s'): %s", cmd, res.Stderr()))
-			identity, err := strconv.Atoi(strings.TrimSpace(res.Stdout()))
-			Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to convert pod identity to an integer (%s)", err))
-			identityKey := fmt.Sprintf("%d %d %d %d",
-				identity&0xff,
-				(identity>>8)&0xff,
-				(identity>>16)&0xff,
-				(identity>>24)&0xff)
+			// the byte-counter hash map.
+			identityKey := getIdentityKey("k8s:id=app1", ciliumPodK8s2)

-			By("Attaching a program on the ingress side (IPv4)")
+			err = kubectl.WaitforPods(helpers.DefaultNamespace, "-l zgroup=testapp", helpers.HelperTimeout)
+			Expect(err).Should(BeNil())

-			checkOneDirection(endpointId, ciliumPodK8s2, podApp2.Name, podApp1.Status.PodIP, identityKey, IngressIPv4)
+			By("Attaching a program on ingress side (IPv4)")
+
+			checkOneDirection(endpointId, ciliumPodK8s2, podApp2.Name,
+				podApp1.Status.PodIP, identityKey, IngressIPv4, pingBytes)
 			cleanupByteCounter(endpointId, ciliumPodK8s2, identityKey, IngressIPv4)

-			By("Attaching a program on the egress side (IPv4)")
+			By("Attaching a program on egress side (IPv4)")

-			checkOneDirection(endpointId, ciliumPodK8s2, podApp2.Name, podApp1.Status.PodIP, identityKey, EgressIPv4)
+			checkOneDirection(endpointId, ciliumPodK8s2, podApp2.Name,
+				podApp1.Status.PodIP, identityKey, EgressIPv4, pingBytes)
 			cleanupByteCounter(endpointId, ciliumPodK8s2, identityKey, EgressIPv4)
 		})
+
+		// Check the ingress hook in tail_ipv4_to_endpoint()
+		// Similar to the above, with endpointRoutes enabled
+		It("Loads byte-counter and gets consistent values, with per-endpoint routes", func() {
+
+			deploymentManager.DeployCilium(map[string]string{
+				"customCalls.enabled":    "true",
+				"endpointRoutes.enabled": "true",
+			}, DeployCiliumOptionsAndDNS)
+
+			ciliumPodK8s2, err := kubectl.GetCiliumPodOnNode(helpers.K8s2)
+			Expect(err).ShouldNot(HaveOccurred(), "Cannot get cilium pod on k8s2")
+
+			copyAndLoadObjectFile(ciliumPodK8s2)
+			defer cleanupLoadedObjects(ciliumPodK8s2)
+
+			getPodsInfo()
+
+			endpoint, err := kubectl.GetCiliumEndpoint(helpers.DefaultNamespace, podApp2.Name)
+			Expect(err).Should(BeNil(), fmt.Sprintf("Failed to retrieve endpoint for pod %s: %s", podApp2.Name, err))
+			Expect(endpoint).ShouldNot(BeNil(), fmt.Sprintf("Retrieved empty endpoint id for pod %s", podApp2.Name))
+			endpointId := endpoint.ID
+
+			identityKey := getIdentityKey("k8s:id=app1", ciliumPodK8s2)
+
+			err = kubectl.WaitforPods(helpers.DefaultNamespace, "-l zgroup=testapp", helpers.HelperTimeout)
+			Expect(err).Should(BeNil())
+
+			By("Attaching a program on ingress side (IPv4)")
+
+			// Packets to pods are processed twice with per-endpoint routes + VXLAN,
+			// accout for it (see GH-14657)
+			expectedByteCount := 2 * pingBytes
+
+			checkOneDirection(endpointId, ciliumPodK8s2, podApp2.Name,
+				podApp1.Status.PodIP, identityKey, IngressIPv4, expectedByteCount)
+			cleanupByteCounter(endpointId, ciliumPodK8s2, identityKey, IngressIPv4)
+		})
 	})
 })

test-me-please

CI test failed because there were two identities with the label k8s:id=app1 and we expected a single one, resulting in a failure to parse an integer. This should now be fixed by retrieving the identity for the endpoint with cilium endpoint list instead of searching the list of identities with cilium identity list. I also refactored the two It() blocks in the CI test, as discussed with Paul. Sorry, I forgot to backup the previous version, so no incremental change this time, but the changes are just those mentioned above.

test-me-please

test-me-please

test-gke

All CI jobs are green. A review from @cilium/kubernetes is missing but I think we can skip it as it was only requested for  install/kubernetes/ and several person familiar with the agent and Helm reviewed. Marking as ready to merge.

Hi @qmonnet, great job! I'm just looking for a traffic byte-counter, and I have a few question:

The byte-counter is implemented using BPF_MAP_TYPE_HASH, would it be more efficient to implement it using BPF_MAP_TYPE_PERCPU_HASH? Just like cilium_metrics.
Does cilium have an existing tool to collect and send customer metrics to Prometheus? Since there is no such command like cilium metrics add.

Looking forward to your reply, much appreciated!

Hi, thanks for using the feature!

The byte-counter is implemented using BPF_MAP_TYPE_HASH, would it be more efficient to implement it using BPF_MAP_TYPE_PERCPU_HASH? Just like cilium_metrics.

I'm not sure what gain you expect here. The updates on the counter value are atomic operations, so it shouldn't be an issue if multiple CPUs work with the same map. This being said, the point of these hooks is to have custom programs, so of course you're free to experiment :)

How to determine if the __ctx_buff is on ingress path or on egress path?

Please have a look at the related README.rst, and in particular at step 3 in the instructions for the byte-counter program. There are several hooks where you can attach the program, some on ingress, some on egress. The current program does not make the distinction between ingress and egress. If you want it, you can edit the program to e.g. use another map for egress (distinct from the one for ingress), and attach this new program to the hooks on egress.

Thanks!
One more question, how to get byte-counter program  to be attached automatically when the cilium_calls_custom_* map is created?

There's nothing of the sort implemented in the agent currently, so I suppose you'd have to create some watcher to check for the presence of the maps and attach the programs when necessary.


[development]

Pull Request Test Coverage Report for Build 50dca52a-e590-488d-beeb-e040ba93d0f8

-1 of 5 (80.0%)  changed or added relevant lines in 3 files are covered.
6 unchanged lines in 3 files lost coverage.
Overall coverage decreased (-0.02%) to 64.085%

Changes Missing Coverage
Covered Lines
Changed/Added Lines
%

src/components/html/input/input.vue
1
2
50.0%

Files with Coverage Reduction
New Missed Lines
%

src/components/html/input/input.vue
1
87.5%

src/components/ui/dropdown/dropdown.vue
2
74.76%

src/components/ui/list/list.vue
3
71.99%

Totals

Change from base Build 3fb59c51-5347-423c-9be6-55fb72ae59fd:
-0.02%

Covered Lines:
517

Relevant Lines:
604

💛  - Coveralls