Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 112 additions & 12 deletions .github/workflows/bpf-focused-mcast-test.yaml
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
name: BPF Focused Multicast EGW Test
name: Multicast EGW Regression Test

on:
pull_request:
branches:
- master
- feat/l2fix-mcast-v3-sip-egress
paths:
- '.github/workflows/bpf-focused-mcast-test.yaml'
- 'bpf/**'
- 'pkg/egressgateway/**'
- 'pkg/maps/egressmap/**'
workflow_dispatch:
inputs:
bpf_test:
description: 'BPF test name (without .c extension)'
required: false
default: 'tc_egressgw_redirect_multicast'

permissions: read-all

jobs:
bpf_focused_test:
name: BPF Focused Multicast EGW Test
multicast_egw_regression:
name: BPF and Go multicast EGW regression
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: feat/multicast-egw-cegp-downstream
persist-credentials: false
fetch-depth: 0

Expand All @@ -39,8 +43,104 @@ jobs:
run: |
pip3 install --quiet scapy Jinja2

- name: Run focused BPF test
- name: Run multicast and unicast CEGP BPF regressions
shell: bash
run: |
set -o pipefail

# Every object that must actually execute in this receipt: the two
# multicast cases plus the pre-existing unicast CEGP regressions.
REQUIRED_OBJECTS=(
tc_egressgw_origin_node_multicast
tc_egressgw_redirect_multicast
tc_egressgw_redirect_from_host
tc_egressgw_redirect_from_overlay
tc_egressgw_redirect_from_overlay_with_egress_interface
tc_egressgw_snat
)

# Remove any pre-existing objects so the post-run assertion below
# proves that *this* invocation compiled and selected each one.
rm -f bpf/tests/tc_egressgw_*.o

make run_bpf_tests \
BPF_TEST="${{ github.event.inputs.bpf_test || 'tc_egressgw_redirect_multicast' }}" \
BPF_TEST_VERBOSE=1
BPF_TEST='tc_egressgw' \
BPF_TEST_VERBOSE=1 \
| tee /tmp/bpf-egressgw-test.log

# Fail closed on the execution set rather than on source presence.
# bpftest enumerates the *.o files under -bpf-test-path and runs every
# one whose name carries the -test prefix, so an object built here is
# an object that ran. Asserting on built objects (not .c sources) is
# what catches a selection regression: BPF_TEST previously expanded to
# `grep tc_egressgw.o`, whose unescaped '.' matched only
# tc_egressgw_origin_node_multicast.o, so five of these six never
# compiled and never ran while the job still reported success.
missing=()
for obj in "${REQUIRED_OBJECTS[@]}"; do
[ -f "bpf/tests/${obj}.o" ] || missing+=("${obj}")
done
if [ "${#missing[@]}" -ne 0 ]; then
echo "These BPF test objects were not built, so they did not run: ${missing[*]}" >&2
exit 1
fi
echo "Verified all ${#REQUIRED_OBJECTS[@]} required tc_egressgw BPF objects were built and executed."

# Parse the tparse summary row for this package and require a tally.
# A missing/unparseable row is itself a failure, so a change in the
# runner's output shape can never silently degrade into a pass.
# Columns are: STATUS | ELAPSED | PACKAGE | COVER | PASS | FAIL | SKIP
# `read` returns non-zero on empty input; swallow that so the explicit
# validation below reports the problem instead of `set -e` aborting
# with no context.
read -r pass fail skip < <(
sed 's/\x1b\[[0-9;]*m//g' /tmp/bpf-egressgw-test.log \
| awk -F'│' '/github.com\/cilium\/cilium\/bpf\/tests\/bpftest/ && NF >= 9 {
for (i = 6; i <= 8; i++) gsub(/^ +| +$/, "", $i);
print $6, $7, $8; exit
}'
) || true
if ! [[ "${pass}" =~ ^[0-9]+$ && "${fail}" =~ ^[0-9]+$ && "${skip}" =~ ^[0-9]+$ ]]; then
echo "Could not parse a numeric BPF test summary row (got pass='${pass}' fail='${fail}' skip='${skip}') — treating as a failure" >&2
exit 1
fi
echo "BPF receipt: PASS=${pass} FAIL=${fail} SKIP=${skip}"

if [ "${pass}" -ne 49 ] || [ "${fail}" -ne 0 ] || [ "${skip}" -ne 0 ]; then
echo "Unexpected BPF egress gateway receipt (expected PASS=49 FAIL=0 SKIP=0; got PASS=${pass} FAIL=${fail} SKIP=${skip})" >&2
exit 1
fi

- name: Run CEGP validation and reconciliation regressions
shell: bash
run: |
set -o pipefail

REQUIRED_GO_TESTS=(
TestParseCEGPMulticastDestinationCIDRs
TestPrivilegedEgressGatewayCEGPParser
TestPrivilegedEgressGatewayManager
TestPrivilegedMulticastEgressGatewayManager
)
test_pattern=$(IFS='|'; echo "${REQUIRED_GO_TESTS[*]}")

sudo -E env "PATH=$PATH" PRIVILEGED_TESTS=1 \
go test -v -count=1 -timeout=10m ./pkg/egressgateway \
-run "^(${test_pattern})$" \
| tee /tmp/multicast-egw-go-test.log

if grep -q -- '--- SKIP:' /tmp/multicast-egw-go-test.log; then
echo 'Privileged reconciliation test unexpectedly skipped' >&2
exit 1
fi

missing=()
for test_name in "${REQUIRED_GO_TESTS[@]}"; do
grep -Eq -- "^--- PASS: ${test_name}( |$)" /tmp/multicast-egw-go-test.log \
|| missing+=("${test_name}")
done
if [ "${#missing[@]}" -ne 0 ]; then
echo "Required Go tests did not report PASS: ${missing[*]}" >&2
exit 1
fi
echo "Verified all ${#REQUIRED_GO_TESTS[@]} required Go tests executed and passed."
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,63 @@ Because this problem is a result of hitting a hard limit on Cilium's Egress Gate
that are being SNATed through an egress-gateway, This can be done by having clients avoid creating as many new connections, or by lowering the amount of connections going to the same remote address (with a common egress IP) by splitting up traffic via different egress IPs and/or remote endpoint addresses.

For alerting and observability on SNAT source port utilization please see the :ref:`NAT endpoint max connection <nat_metrics>` metric which tracks the top saturation (as a percentage of total the max available) of a Cilium Agent.

Verify downstream multicast egress
----------------------------------

The downstream multicast Egress Gateway extension can be verified on a staging
cluster with at least two nodes using VXLAN encapsulation. The selected gateway
node must have the policy's ``egressIP`` assigned to its external interface and
that address must be routable on the receiver-facing network. These L2 and
routing prerequisites are the exact reason this check is not run on a standard
GitHub-hosted runner.

Set the values for the deployed policy and publisher:

.. code-block:: shell-session

$ export GATEWAY_NODE=worker-gateway
$ export EXTERNAL_IFACE=eth0
$ export EGRESS_IP=198.51.100.128
$ export POLICY_CIDR=232.0.0.0/8
$ export GROUP=232.1.1.50
$ export PORT=5000
$ export PUBLISHER_POD=multicast-publisher

Confirm that Cilium reconciled the multicast policy with an external interface.
The row must report ``multicast``, ``ct-bypass``, and a non-zero egress ifindex:

.. code-block:: shell-session

$ kubectl -n kube-system exec $(kubectl -n kube-system get pod -l k8s-app=cilium --field-selector spec.nodeName=${GATEWAY_NODE} -o name) -c cilium-agent -- \
cilium-dbg bpf egress list | grep "${POLICY_CIDR}"
10.244.1.25 232.0.0.0/8 198.51.100.128 10.0.0.12 multicast ct-bypass 2

Start a capture on the gateway node's external interface:

.. code-block:: shell-session

$ kubectl debug node/${GATEWAY_NODE} -it --image=nicolaka/netshoot -- \
tcpdump -ni ${EXTERNAL_IFACE} -vv -c 5 "udp and dst host ${GROUP} and dst port ${PORT}"

In another terminal, publish five datagrams from a pod selected by the
``CiliumEgressGatewayPolicy``:

.. code-block:: shell-session

$ kubectl exec ${PUBLISHER_POD} -- sh -c \
'for n in 1 2 3 4 5; do printf "multicast-egw-%s\n" "$n" | nc -u -w1 "'$GROUP'" "'$PORT'"; done'

The capture must show the policy's egress IP as the source and preserve the
multicast destination. The publisher pod IP or origin-node IP must not appear as
the source:

.. code-block:: text

IP 198.51.100.128.40000 > 232.1.1.50.5000: UDP, length 16
IP 198.51.100.128.40001 > 232.1.1.50.5000: UDP, length 16

Record the Cilium image digest, policy YAML, gateway node, external interface,
and full tcpdump output with the test receipt. A capture showing
``src=<egressIP>, dst=232.x.y.z`` is the acceptance signal; zero packets, a
different source, or packets on a non-gateway node is a failure.
12 changes: 10 additions & 2 deletions bpf/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ ALL_TEST_OBJECTS = $(patsubst %.c, %.o, $(wildcard *.c))
ifeq ($(origin BPF_TEST), undefined)
TEST_OBJECTS = $(ALL_TEST_OBJECTS)
else
TEST_OBJECTS=$(shell echo $(ALL_TEST_OBJECTS) | tr ' ' '\n' | grep $(BPF_TEST).o)
# BPF_TEST is matched as a substring of the test object names. The list is
# already restricted to *.o, so no suffix anchor is needed -- and anchoring on
# an unescaped ".o" made the '.' a regex wildcard, so a prefix selection such
# as BPF_TEST=tc_egressgw silently matched only the objects that happen to
# have a character followed by an 'o' right after the prefix (e.g.
# tc_egressgw_origin_node_multicast.o) while dropping every sibling object.
# That also made this build-time selection disagree with the runner's
# -test filter below, which treats BPF_TEST as a plain literal prefix.
TEST_OBJECTS=$(shell echo $(ALL_TEST_OBJECTS) | tr ' ' '\n' | grep -- "$(BPF_TEST)")
ifeq ($(strip $(TEST_OBJECTS)),)
$(error Invalid test '$(BPF_TEST).o')
$(error Invalid test '$(BPF_TEST)')
endif
endif

Expand Down
2 changes: 0 additions & 2 deletions bpf/tests/tc_egressgw_origin_node_multicast.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,3 @@ int egressgw_origin_node_mcast_classify(const struct __ctx_buff *ctx __maybe_unu

test_finish();
}

BPF_LICENSE("Dual BSD/GPL");
11 changes: 8 additions & 3 deletions pkg/egressgateway/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,26 +742,31 @@ func (manager *Manager) updateEgressRules6() {
if excludedCIDR {
gatewayIP = ExcludedCIDRIPv4
}
egressIfindex := uint32(0)
if dstCIDR.Addr().IsMulticast() {
egressIfindex = gwc.egressIfindex
}

if policyPresent && policyVal.Match(gwc.egressIP6, gatewayIP, gwc.egressIfindex) {
if policyPresent && policyVal.Match(gwc.egressIP6, gatewayIP, egressIfindex) {
return
}

if err := manager.policyMap6.Update(endpointIP, dstCIDR, gwc.egressIP6, gatewayIP, gwc.egressIfindex); err != nil {
if err := manager.policyMap6.Update(endpointIP, dstCIDR, gwc.egressIP6, gatewayIP, egressIfindex); err != nil {
manager.logger.Error(
"Error applying IPv6 egress gateway policy",
logfields.Error, err,
logfields.SourceIP, endpointIP,
logfields.DestinationCIDR, dstCIDR,
logfields.EgressIP, gwc.egressIP6,
logfields.LinkIndex, gwc.egressIfindex,
logfields.LinkIndex, egressIfindex,
logfields.GatewayIP, gatewayIP,
)
} else {
manager.logger.Debug("IPv6 egress gateway policy applied",
logfields.SourceIP, endpointIP,
logfields.DestinationCIDR, dstCIDR,
logfields.EgressIP, gwc.egressIP6,
logfields.LinkIndex, egressIfindex,
logfields.GatewayIP, gatewayIP,
)
}
Expand Down
Loading
Loading