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
46 changes: 46 additions & 0 deletions .github/workflows/bpf-focused-mcast-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: BPF Focused Multicast EGW Test

on:
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
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: feat/multicast-egw-cegp-downstream
persist-credentials: false
fetch-depth: 0

- name: Git status and log
run: |
echo "=== git status ==="
git status --short --branch
echo ""
echo "=== git log ==="
git log --oneline -5

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'

- name: Install Python deps for Scapy packet generation
run: |
pip3 install --quiet scapy Jinja2

- name: Run focused BPF test
run: |
make run_bpf_tests \
BPF_TEST="${{ github.event.inputs.bpf_test || 'tc_egressgw_redirect_multicast' }}" \
BPF_TEST_VERBOSE=1
103 changes: 100 additions & 3 deletions bpf/bpf_lxc.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "lib/l3.h"
#include "lib/local_delivery.h"
#include "lib/lxc.h"
#include "lib/l2_responder.h"
#include "lib/lrp.h"
#include "lib/identity.h"
#include "lib/policy.h"
Expand All @@ -59,6 +60,70 @@
#include "lib/vtep.h"
#include "lib/subnet.h"

/* is_valid_l2_announced_ip{,v4} — L2-announced VIP source-IP fallthrough.
*
* Pairs with is_valid_lxc_src_ip{,v4} (lib/lxc.h, ENABLE_SIP_VERIFICATION):
* the strict (saddr == endpoint_ip) check runs first; on miss, callers also
* probe the L2 responder map. Pods running the kernel AMT relay
* (drivers/net/amt.c) source packets with a Service ExternalIP/VIP per
* RFC 7450 §5.1.2. Mirror b5b3b4b908 (sock4_skip_xlate): trust addresses
* present in the L2 responder map for direct_routing_dev_ifindex on this
* node.
*
* Kept here (not in lib/lxc.h) so lib/l2_responder.h stays out of lxc.h's
* include chain — l2_responder.h carries handle_l2_announcement, which
* references arp/icmp6/runtime-config symbols only available where arp.h
* + icmp6.h + the per-endpoint config are already pulled. bpf_lxc.c
* already pulls those; tests and other lxc.h consumers don't.
*
* Operational caveat: lookup only succeeds on the node currently holding
* the L2 announce lease for the VIP. Pods on non-holder nodes still see
* DROP_INVALID_SIP — colocate with the lease holder via nodeSelector or
* use hostNetwork.
*/
#ifdef ENABLE_SIP_VERIFICATION
static __always_inline bool
is_valid_l2_announced_ipv4(const struct iphdr *ip4 __maybe_unused)
{
#ifdef ENABLE_IPV4
struct l2_responder_v4_key l2key = {
.ip4 = ip4->saddr,
.ifindex = CONFIG(direct_routing_dev_ifindex),
};

return map_lookup_elem(&cilium_l2_responder_v4, &l2key) != NULL;
#else
return false;
#endif
}

static __always_inline bool
is_valid_l2_announced_ipv6(struct ipv6hdr *ip6 __maybe_unused)
{
#ifdef ENABLE_IPV6
struct l2_responder_v6_key l2key = {};

l2key.ifindex = CONFIG(direct_routing_dev_ifindex);
ipv6_addr_copy(&l2key.ip6, (union v6addr *)&ip6->saddr);
return map_lookup_elem(&cilium_l2_responder_v6, &l2key) != NULL;
#else
return false;
#endif
}
#else /* !ENABLE_SIP_VERIFICATION */
static __always_inline bool
is_valid_l2_announced_ipv4(const struct iphdr *ip4 __maybe_unused)
{
return true;
}

static __always_inline bool
is_valid_l2_announced_ipv6(struct ipv6hdr *ip6 __maybe_unused)
{
return true;
}
#endif /* ENABLE_SIP_VERIFICATION */

#if defined(ENABLE_HOST_FIREWALL) && !defined(ENABLE_ROUTING)
static __always_inline int
lxc_deliver_to_host(struct __ctx_buff *ctx, __u32 src_sec_identity)
Expand Down Expand Up @@ -1005,7 +1070,9 @@ static __always_inline int __tail_handle_ipv6(struct __ctx_buff *ctx,
#ifdef ENABLE_L7_LB
from_l7lb = ctx_load_meta(ctx, CB_FROM_HOST) == FROM_HOST_L7_LB;
#endif
if (!from_l7lb && unlikely(!is_valid_lxc_src_ip(ip6)))
if (!from_l7lb &&
unlikely(!is_valid_lxc_src_ip(ip6) &&
!is_valid_l2_announced_ipv6(ip6)))
return DROP_INVALID_SIP;

#ifdef ENABLE_PER_PACKET_LB
Expand Down Expand Up @@ -1512,6 +1579,16 @@ static __always_inline int handle_ipv4_from_lxc(struct __ctx_buff *ctx, __u32 *d
return DROP_UNKNOWN_CT;
}

#ifdef ENABLE_EGRESS_GATEWAY_COMMON
if (egw_ipv4_is_mcast(ip4->daddr)) {
ret = egress_gw_handle_request(ctx, bpf_htons(ETH_P_IP),
SECLABEL_IPV4, *dst_sec_identity,
&trace);
if (ret != CTX_ACT_OK)
return ret;
}
#endif

return ipv4_forward_to_destination(ctx, ip4, tuple, *dst_sec_identity,
ct_state, ct_status, info, skip_tunnel,
hairpin_flow, from_l7lb, proxy_port,
Expand Down Expand Up @@ -1563,7 +1640,9 @@ static __always_inline int __tail_handle_ipv4(struct __ctx_buff *ctx,
#ifdef ENABLE_L7_LB
from_l7lb = ctx_load_meta(ctx, CB_FROM_HOST) == FROM_HOST_L7_LB;
#endif
if (!from_l7lb && unlikely(!is_valid_lxc_src_ipv4(ip4)))
if (!from_l7lb &&
unlikely(!is_valid_lxc_src_ipv4(ip4) &&
!is_valid_l2_announced_ipv4(ip4)))
return DROP_INVALID_SIP;

#ifdef ENABLE_MULTICAST
Expand All @@ -1576,7 +1655,25 @@ static __always_inline int __tail_handle_ipv4(struct __ctx_buff *ctx,
}

if (IN_MULTICAST(bpf_ntohl(ip4->daddr))) {
if (mcast_lookup_subscriber_map(&ip4->daddr))
/* Origin-node multicast EGW classification (BLO-8007).
*
* A pod-originated multicast destination that matches a multicast
* EgressGatewayPolicy must leave via the egress gateway, so it must
* NOT be short-circuited into the cluster-internal subscriber-map
* fast path here - that is the host/local emission that the egress
* classification has to win against. We therefore consult the EGW
* policy map first and only fall through to local delivery when the
* destination is *not* a multicast CEGP hit. On a hit we let the
* packet continue down the normal egress path (per-packet LB -> CT
* egress -> handle_ipv4_from_lxc), where the existing, already
* multicast-aware EGW redirect/SNAT machinery takes over.
*
* Non-matching multicast (no policy, excluded CIDR, or no gateway)
* keeps the pre-existing local multicast behavior, and unicast is
* untouched (egw_mcast_request_is_egress() is multicast-only).
*/
if (!egw_mcast_request_is_egress(ip4->saddr, ip4->daddr) &&
mcast_lookup_subscriber_map(&ip4->daddr))
return tail_call_internal(ctx,
CILIUM_CALL_MULTICAST_EP_DELIVERY,
ext_err);
Expand Down
21 changes: 21 additions & 0 deletions bpf/bpf_overlay.c
Original file line number Diff line number Diff line change
Expand Up @@ -385,17 +385,38 @@ static __always_inline int handle_ipv4(struct __ctx_buff *ctx,
{
__u32 egress_ifindex = 0;
__be32 snat_addr, daddr;
__be32 saddr;
__u8 nexthdr;
int l4_off;
fraginfo_t mcast_fraginfo;

saddr = ip4->saddr;
daddr = ip4->daddr;
nexthdr = ip4->protocol;
l4_off = ETH_HLEN + ipv4_hdrlen(ip4);
mcast_fraginfo = ipfrag_encode_ipv4(ip4);

if (egress_gw_snat_needed_hook(ip4->saddr, daddr, &snat_addr,
&egress_ifindex)) {
if (snat_addr == EGRESS_GATEWAY_NO_EGRESS_IP)
return DROP_NO_EGRESS_IP;
if (egw_ipv4_is_mcast(daddr) && !egress_ifindex)
return DROP_NO_FIB;

ret = ipv4_l3(ctx, ETH_HLEN, NULL, NULL, ip4);
if (unlikely(ret != CTX_ACT_OK))
return ret;

if (egw_ipv4_is_mcast(daddr)) {
ret = snat_v4_rewrite_headers(ctx, nexthdr, ETH_HLEN,
ipfrag_has_l4_header(mcast_fraginfo),
l4_off, saddr, snat_addr,
offsetof(struct iphdr, saddr), 0,
0, 0, 0);
if (unlikely(ret < 0))
return ret;
}

set_identity_mark(ctx, *identity, MARK_MAGIC_EGW_DONE);

/* to-netdev@bpf_host handles SNAT, so no need to do it here. */
Expand Down
51 changes: 48 additions & 3 deletions bpf/lib/egress_gateway.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ struct egress_gw_policy_key {
struct egress_gw_policy_entry {
__be32 egress_ip;
__be32 gateway_ip;
__u32 egress_ifindex;
__u32 reserved;
};

struct egress_gw_policy_key6 {
Expand Down Expand Up @@ -165,6 +167,51 @@ egress_gw_request_needs_redirect(struct ipv4_ct_tuple *rtuple __maybe_unused,
#endif /* ENABLE_EGRESS_GATEWAY */
}

/* egw_mcast_request_is_egress - origin-node classification for pod-originated
* IPv4 multicast (BLO-8007, downstream-only).
*
* Returns true when @daddr is a multicast destination that matches a multicast
* EgressGatewayPolicy with a real gateway, i.e. the packet must leave the node
* via the egress gateway instead of the cluster-internal multicast fast path.
*
* Returns false for non-multicast destinations, for multicast with no matching
* policy (or an excluded-CIDR / no-gateway policy), and when EGW is compiled
* out. In every false case the caller keeps the pre-existing behavior, so this
* helper can only ever *divert* a multicast destination that an operator has
* explicitly placed under a policy - it never changes unicast handling.
*
* Downstream divergence from upstream Cilium: upstream never consults the EGW
* policy map for multicast destinations because the from-container path short-
* circuits IN_MULTICAST traffic to local delivery before any EGW lookup can
* run. We deliberately add this multicast-only lookup on the origin node so
* multicast CEGP hits are classified before local emission. The unicast
* gateway_ip sentinels (NO_GATEWAY / EXCLUDED_CIDR) are honored unchanged.
*/
static __always_inline bool
egw_mcast_request_is_egress(__be32 saddr __maybe_unused, __be32 daddr __maybe_unused)
{
#if defined(ENABLE_EGRESS_GATEWAY)
const struct egress_gw_policy_entry *egress_gw_policy;

if (!egw_ipv4_is_mcast(daddr))
return false;

egress_gw_policy = lookup_ip4_egress_gw_policy(saddr, daddr);
if (!egress_gw_policy)
return false;

switch (egress_gw_policy->gateway_ip) {
case EGRESS_GATEWAY_NO_GATEWAY:
case EGRESS_GATEWAY_EXCLUDED_CIDR:
return false;
}

return true;
#else
return false;
#endif /* ENABLE_EGRESS_GATEWAY */
}

static __always_inline
bool egress_gw_snat_needed(__be32 saddr __maybe_unused,
__be32 daddr __maybe_unused,
Expand All @@ -183,9 +230,7 @@ bool egress_gw_snat_needed(__be32 saddr __maybe_unused,
return false;

*snat_addr = egress_gw_policy->egress_ip;
#ifdef EGRESS_IFINDEX
*egress_ifindex = EGRESS_IFINDEX;
#endif
*egress_ifindex = egress_gw_policy->egress_ifindex;

return true;
#else
Expand Down
3 changes: 3 additions & 0 deletions bpf/tests/lib/egressgw_policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ static __always_inline void add_egressgw_policy_entry(__be32 saddr, __be32 daddr
struct egress_gw_policy_entry in_val = {
.egress_ip = egress_ip,
.gateway_ip = gateway_ip,
#ifdef EGRESS_IFINDEX
.egress_ifindex = EGRESS_IFINDEX,
#endif
};

map_update_elem(&cilium_egress_gw_policy_v4, &in_key, &in_val, 0);
Expand Down
Loading
Loading