diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md index 9f77cb76f272a..165f37058e1f9 100644 --- a/Documentation/cmdref/cilium-agent.md +++ b/Documentation/cmdref/cilium-agent.md @@ -445,6 +445,7 @@ cilium-agent [flags] --vtep-mac strings List of VTEP MAC addresses for forwarding traffic outside the cluster --vtep-mask string VTEP CIDR Mask for all VTEP CIDRs (default "255.255.255.0") --vtep-sync-interval duration Interval for VTEP sync (default 1m0s) + --vtep-policy-reconciliation-trigger-interval duration Time between triggers of vtep policy state reconciliations (default 1s) --wireguard-persistent-keepalive duration The Wireguard keepalive interval as a Go duration string --write-cni-conf-when-ready string Write the CNI configuration to the specified path when agent is ready --ztunnel-zds-unix-addr string Unix address for zds server (default "/var/run/cilium/ztunnel.sock") diff --git a/Documentation/cmdref/cilium-agent_hive.md b/Documentation/cmdref/cilium-agent_hive.md index 4b337c9f698aa..7cc0bf738c9d1 100644 --- a/Documentation/cmdref/cilium-agent_hive.md +++ b/Documentation/cmdref/cilium-agent_hive.md @@ -273,6 +273,7 @@ cilium-agent hive [flags] --vtep-endpoint strings List of VTEP IP addresses --vtep-mac strings List of VTEP MAC addresses for forwarding traffic outside the cluster --vtep-sync-interval duration Interval for VTEP sync (default 1m0s) + --vtep-policy-reconciliation-trigger-interval duration Time between triggers of vtep policy state reconciliations (default 1s) --wireguard-persistent-keepalive duration The Wireguard keepalive interval as a Go duration string --write-cni-conf-when-ready string Write the CNI configuration to the specified path when agent is ready --ztunnel-zds-unix-addr string Unix address for zds server (default "/var/run/cilium/ztunnel.sock") diff --git a/Documentation/cmdref/cilium-agent_hive_dot-graph.md b/Documentation/cmdref/cilium-agent_hive_dot-graph.md index 000d96fc07772..b607a12a8a1cb 100644 --- a/Documentation/cmdref/cilium-agent_hive_dot-graph.md +++ b/Documentation/cmdref/cilium-agent_hive_dot-graph.md @@ -278,6 +278,7 @@ cilium-agent hive dot-graph [flags] --vtep-endpoint strings List of VTEP IP addresses --vtep-mac strings List of VTEP MAC addresses for forwarding traffic outside the cluster --vtep-sync-interval duration Interval for VTEP sync (default 1m0s) + --vtep-policy-reconciliation-trigger-interval duration Time between triggers of vtep policy state reconciliations (default 1s) --wireguard-persistent-keepalive duration The Wireguard keepalive interval as a Go duration string --write-cni-conf-when-ready string Write the CNI configuration to the specified path when agent is ready --ztunnel-zds-unix-addr string Unix address for zds server (default "/var/run/cilium/ztunnel.sock") diff --git a/Documentation/cmdref/cilium-dbg_bpf.md b/Documentation/cmdref/cilium-dbg_bpf.md index ff6b015d73d6e..b5e6ae0f93f3b 100644 --- a/Documentation/cmdref/cilium-dbg_bpf.md +++ b/Documentation/cmdref/cilium-dbg_bpf.md @@ -43,4 +43,5 @@ Direct access to local BPF maps * [cilium-dbg bpf socknat](cilium-dbg_bpf_socknat.md) - Socket NAT operations * [cilium-dbg bpf srv6](cilium-dbg_bpf_srv6.md) - Manage the SRv6 routing rules * [cilium-dbg bpf vtep](cilium-dbg_bpf_vtep.md) - Manage the VTEP mappings for IP/CIDR <-> VTEP MAC/IP +* [cilium-dbg bpf vtep-policy](cilium-dbg_bpf_vtep-policy.md) - Manage the VTEP Policy mappings diff --git a/Documentation/cmdref/cilium-dbg_bpf_vtep-policy.md b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy.md new file mode 100644 index 0000000000000..fb5947ed6552c --- /dev/null +++ b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy.md @@ -0,0 +1,29 @@ + + +## cilium-dbg bpf vtep-policy + +Manage the VTEP Policy mappings + +### Options + +``` + -h, --help help for vtep-policy +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cilium.yaml) + -D, --debug Enable debug messages + -H, --host string URI to server-side API + --log-driver strings Logging endpoints to use (example: syslog) + --log-opt map Log driver options (example: format=json) +``` + +### SEE ALSO + +* [cilium-dbg bpf](cilium-dbg_bpf.md) - Direct access to local BPF maps +* [cilium-dbg bpf vtep-policy delete](cilium-dbg_bpf_vtep-policy_delete.md) - Delete VTEP Policy entries +* [cilium-dbg bpf vtep-policy list](cilium-dbg_bpf_vtep-policy_list.md) - List VTEP Policy entries +* [cilium-dbg bpf vtep-policy update](cilium-dbg_bpf_vtep-policy_update.md) - Update VTEP Policy entries + diff --git a/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_delete.md b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_delete.md new file mode 100644 index 0000000000000..0d6aa4f2c9af4 --- /dev/null +++ b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_delete.md @@ -0,0 +1,35 @@ + + +## cilium-dbg bpf vtep-policy delete + +Delete VTEP Policy entries + +### Synopsis + +Delete vtep entries using vtep CIDR. + + +``` +cilium-dbg bpf vtep-policy delete [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cilium.yaml) + -D, --debug Enable debug messages + -H, --host string URI to server-side API + --log-driver strings Logging endpoints to use (example: syslog) + --log-opt map Log driver options (example: format=json) +``` + +### SEE ALSO + +* [cilium-dbg bpf vtep-policy](cilium-dbg_bpf_vtep-policy.md) - Manage the VTEP Policy mappings + diff --git a/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_list.md b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_list.md new file mode 100644 index 0000000000000..aeb65423bd8ec --- /dev/null +++ b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_list.md @@ -0,0 +1,36 @@ + + +## cilium-dbg bpf vtep-policy list + +List VTEP Policy entries + +### Synopsis + +List VTEP CIDR and their corresponding VTEP MAC/IP. + + +``` +cilium-dbg bpf vtep-policy list [flags] +``` + +### Options + +``` + -h, --help help for list + -o, --output string json| yaml| jsonpath='{}' +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cilium.yaml) + -D, --debug Enable debug messages + -H, --host string URI to server-side API + --log-driver strings Logging endpoints to use (example: syslog) + --log-opt map Log driver options (example: format=json) +``` + +### SEE ALSO + +* [cilium-dbg bpf vtep-policy](cilium-dbg_bpf_vtep-policy.md) - Manage the VTEP Policy mappings + diff --git a/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_update.md b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_update.md new file mode 100644 index 0000000000000..2038f1b7ccceb --- /dev/null +++ b/Documentation/cmdref/cilium-dbg_bpf_vtep-policy_update.md @@ -0,0 +1,35 @@ + + +## cilium-dbg bpf vtep-policy update + +Update VTEP Policy entries + +### Synopsis + +Create/Update vtep entry. + + +``` +cilium-dbg bpf vtep-policy update [flags] +``` + +### Options + +``` + -h, --help help for update +``` + +### Options inherited from parent commands + +``` + --config string Config file (default is $HOME/.cilium.yaml) + -D, --debug Enable debug messages + -H, --host string URI to server-side API + --log-driver strings Logging endpoints to use (example: syslog) + --log-opt map Log driver options (example: format=json) +``` + +### SEE ALSO + +* [cilium-dbg bpf vtep-policy](cilium-dbg_bpf_vtep-policy.md) - Manage the VTEP Policy mappings + diff --git a/Documentation/crdlist.rst b/Documentation/crdlist.rst index e7ec380f25651..950462e2e7fa4 100644 --- a/Documentation/crdlist.rst +++ b/Documentation/crdlist.rst @@ -19,3 +19,4 @@ - CiliumNode - CiliumNodeConfig - CiliumPodIPPool +- CiliumVtepPolicy diff --git a/bpf/bpf_alignchecker.c b/bpf/bpf_alignchecker.c index 674f5cf1183b6..93d169a31c228 100644 --- a/bpf/bpf_alignchecker.c +++ b/bpf/bpf_alignchecker.c @@ -89,6 +89,7 @@ add_type(struct egress_gw_policy_entry6); #include "lib/vtep.h" add_type(struct vtep_key); +add_type(struct vtep_policy_key); add_type(struct vtep_value); add_type(struct srv6_vrf_key4); diff --git a/bpf/bpf_host.c b/bpf/bpf_host.c index 59c89fa16e2bd..07e4c6fb2e9a7 100644 --- a/bpf/bpf_host.c +++ b/bpf/bpf_host.c @@ -56,6 +56,7 @@ #include "lib/wireguard.h" #include "lib/l2_responder.h" #include "lib/vtep.h" +#include "lib/crap.h" #include "lib/subnet.h" #define host_egress_policy_hook(ctx, src_sec_identity, ext_err) CTX_ACT_OK @@ -708,6 +709,29 @@ handle_ipv4_cont(struct __ctx_buff *ctx, __u32 secctx, const bool from_host, if (!revalidate_data(ctx, &data, &data_end, &ip4)) return DROP_INVALID; + struct crap_key key; + struct crap_value *tv; + + key.dst_ip = ip4->daddr; + + tv = map_lookup_elem (&cilium_crap_map, &key); + if (tv) { + ep = __lookup_ip4_endpoint(tv->pod_ip); + if (ep) { + int l3_off = ETH_HLEN; + + return ipv4_local_delivery(ctx, l3_off, secctx, MARK_MAGIC_IDENTITY, ip4, ep, + METRIC_INGRESS, true, false, 0); + } + + info = lookup_ip4_remote_endpoint(tv->pod_ip, 0); + if (info) { + return encap_and_redirect_with_nodeid(ctx, info, secctx, + info->sec_identity, &trace, + bpf_htons(ETH_P_IP)); + } + } + #ifdef ENABLE_HOST_FIREWALL from_host_raw = ctx_load_and_clear_meta(ctx, CB_FROM_HOST); diff --git a/bpf/bpf_lxc.c b/bpf/bpf_lxc.c index 3faf2d4531f32..9969c74ecb3d3 100644 --- a/bpf/bpf_lxc.c +++ b/bpf/bpf_lxc.c @@ -56,6 +56,7 @@ #include "lib/fib.h" #include "lib/nodeport.h" #include "lib/policy_log.h" +#include "lib/crap.h" #include "lib/vtep.h" #include "lib/subnet.h" @@ -1171,12 +1172,24 @@ ipv4_forward_to_destination(struct __ctx_buff *ctx, struct iphdr *ip4, #if defined(ENABLE_VTEP) { struct vtep_key vkey = {}; + struct vtep_policy_key vpkey = { + .prefixlen = 64, + .src_ip = ip4->saddr, + .dst_ip = ip4->daddr, + }; struct vtep_value *vtep; vkey.vtep_ip = ip4->daddr & CONFIG(vtep_mask); vtep = map_lookup_elem(&cilium_vtep_map, &vkey); - if (!vtep) - goto skip_vtep; + if (!vtep) { + if (!info || info->sec_identity == WORLD_IPV4_ID) { + vtep = map_lookup_elem(&cilium_vtep_policy_map, &vpkey); + if (!vtep) + goto skip_vtep; + } else { + goto skip_vtep; + } + } if (vtep->vtep_mac && vtep->tunnel_endpoint) { if (eth_store_daddr(ctx, (__u8 *)&vtep->vtep_mac, 0) < 0) @@ -1550,6 +1563,16 @@ static __always_inline int __tail_handle_ipv4(struct __ctx_buff *ctx, if (!revalidate_data_pull(ctx, &data, &data_end, &ip4)) return DROP_INVALID; + struct crap_key key; + struct crap_value *tv; + + key.dst_ip = ip4->saddr; + + tv = map_lookup_elem (&cilium_crap_map, &key); + if (tv) { + return CTX_ACT_OK; + } + /* If IPv4 fragmentation is disabled * AND a IPv4 fragmented packet is received, * then drop the packet. diff --git a/bpf/bpf_overlay.c b/bpf/bpf_overlay.c index 274e5ebae5303..4bde839cdeff9 100644 --- a/bpf/bpf_overlay.c +++ b/bpf/bpf_overlay.c @@ -42,6 +42,7 @@ #include "lib/clustermesh.h" #include "lib/egress_gateway.h" #include "lib/tailcall.h" +#include "lib/crap.h" #include "lib/vtep.h" #include "lib/arp.h" #include "lib/encap.h" @@ -291,6 +292,22 @@ static __always_inline int handle_ipv4(struct __ctx_buff *ctx, if (!revalidate_data_pull(ctx, &data, &data_end, &ip4)) return DROP_INVALID; + struct crap_key key; + struct crap_value *tv; + + key.dst_ip = ip4->daddr; + + tv = map_lookup_elem (&cilium_crap_map, &key); + if (tv) { + ep = __lookup_ip4_endpoint(tv->pod_ip); + if (ep) { + int l3_off = ETH_HLEN; + + return ipv4_local_delivery(ctx, l3_off, SECLABEL_IPV4, MARK_MAGIC_IDENTITY, ip4, ep, + METRIC_INGRESS, true, false, 0); + } + } + /* If IPv4 fragmentation is disabled * AND a IPv4 fragmented packet is received, * then drop the packet. diff --git a/bpf/lib/crap.h b/bpf/lib/crap.h new file mode 100644 index 0000000000000..a8faafc33f9fb --- /dev/null +++ b/bpf/lib/crap.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */ +/* Copyright Authors of Cilium */ + +#pragma once + +#include +#include +#include +#include + +struct crap_key { + __u32 dst_ip; +}; + +struct crap_value { + __u32 pod_ip; +}; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, struct crap_key); + __type(value, struct crap_value); + __uint(pinning, LIBBPF_PIN_BY_NAME); + __uint(max_entries, 8192); + __uint(map_flags, BPF_F_NO_PREALLOC); +} cilium_crap_map __section_maps_btf; diff --git a/bpf/lib/vtep.h b/bpf/lib/vtep.h index 2be7286b6b883..ac94a6e46308b 100644 --- a/bpf/lib/vtep.h +++ b/bpf/lib/vtep.h @@ -12,6 +12,12 @@ struct vtep_key { __u32 vtep_ip; }; +struct vtep_policy_key { + __u32 prefixlen; + __u32 src_ip; + __u32 dst_ip; +}; + struct vtep_value { __u64 vtep_mac; __u32 tunnel_endpoint; @@ -26,6 +32,15 @@ struct { __uint(max_entries, VTEP_MAP_SIZE); __uint(map_flags, CONDITIONAL_PREALLOC); } cilium_vtep_map __section_maps_btf; + +struct { + __uint(type, BPF_MAP_TYPE_LPM_TRIE); + __type(key, struct vtep_policy_key); + __type(value, struct vtep_value); + __uint(pinning, LIBBPF_PIN_BY_NAME); + __uint(max_entries, VTEP_POLICY_MAP_SIZE); + __uint(map_flags, BPF_F_NO_PREALLOC); +} cilium_vtep_policy_map __section_maps_btf; #endif /* ENABLE_VTEP */ DECLARE_CONFIG(__u32, vtep_mask, "VXLAN tunnel endpoint network mask") diff --git a/bpf/node_config.h b/bpf/node_config.h index 61fc1a1cd820a..c4e1a8edd9006 100644 --- a/bpf/node_config.h +++ b/bpf/node_config.h @@ -111,6 +111,7 @@ #define THROTTLE_MAP_SIZE 65536 #define ENABLE_ARP_RESPONDER #define VTEP_MAP_SIZE 8 +#define VTEP_POLICY_MAP_SIZE 16384 #define ENDPOINTS_MAP_SIZE 65536 #define METRICS_MAP_SIZE 65536 #define CILIUM_NET_MAC { .addr = { 0xce, 0x72, 0xa7, 0x03, 0x88, 0x57 } } diff --git a/bugtool/cmd/configuration.go b/bugtool/cmd/configuration.go index 892e624573c37..921b135799371 100644 --- a/bugtool/cmd/configuration.go +++ b/bugtool/cmd/configuration.go @@ -132,6 +132,7 @@ var bpfMapsPath = []string{ "tc/globals/cilium_snat_v4_alloc_retries", "tc/globals/cilium_snat_v6_alloc_retries", "tc/globals/cilium_vtep_map", + "tc/globals/cilium_vtep_policy_map", "tc/globals/cilium_l2_responder_v4", "tc/globals/cilium_l2_responder_v6", "tc/globals/cilium_ratelimit", diff --git a/cilium-dbg/cmd/bpf_crap.go b/cilium-dbg/cmd/bpf_crap.go new file mode 100644 index 0000000000000..4950b007d0ea7 --- /dev/null +++ b/cilium-dbg/cmd/bpf_crap.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "github.com/spf13/cobra" +) + +// BPFCrapCmd represents the bpf command +var BPFCrapCmd = &cobra.Command{ + Use: "crap", + Short: "Manage the CRAP rules", +} + +func init() { + BPFCmd.AddCommand(BPFCrapCmd) +} diff --git a/cilium-dbg/cmd/bpf_crap_delete.go b/cilium-dbg/cmd/bpf_crap_delete.go new file mode 100644 index 0000000000000..de649ca65b3b2 --- /dev/null +++ b/cilium-dbg/cmd/bpf_crap_delete.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "net/netip" + + "github.com/spf13/cobra" + + "github.com/cilium/cilium/pkg/common" + "github.com/cilium/cilium/pkg/maps/crap" +) + +const ( + crapRuleDelUsage = "Delete the CRAP rule.\n" +) + +var bpfCrapDeleteCmd = &cobra.Command{ + Args: cobra.ExactArgs(1), + Use: "delete", + Short: "Delete crap entries", + Long: crapRuleDelUsage, + Run: func(cmd *cobra.Command, args []string) { + common.RequireRootPrivilege("cilium bpf crap delete ") + + vtep, err := crap.OpenPinnedCrapMap(log) + if err != nil { + Fatalf("Unable to open map: %s", err) + } + + dst_ip, err := netip.ParseAddr(args[0]) + if err != nil { + Fatalf("Unable to parse IP '%s'", args[0]) + } + + if err := vtep.RemoveCrapMapping(dst_ip); err != nil { + Fatalf("error deleting contents of map: %s\n", err) + } + }, +} + +func init() { + BPFCrapCmd.AddCommand(bpfCrapDeleteCmd) +} diff --git a/cilium-dbg/cmd/bpf_crap_list.go b/cilium-dbg/cmd/bpf_crap_list.go new file mode 100644 index 0000000000000..e505496cee509 --- /dev/null +++ b/cilium-dbg/cmd/bpf_crap_list.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/cilium/cilium/pkg/command" + "github.com/cilium/cilium/pkg/common" + "github.com/cilium/cilium/pkg/maps/crap" +) + +const ( + crapRuleCidrTitle = "Public IP" + crapRuleTitle = "Pod IP" +) + +var ( + crapRuleListUsage = "List CRAP public IPs and their corresponding pod IPs.\n" +) + +var bpfCrapListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List crap entries", + Long: crapRuleListUsage, + Run: func(cmd *cobra.Command, args []string) { + common.RequireRootPrivilege("cilium bpf crap list") + + vtep, err := crap.OpenPinnedCrapMap(log) + if err != nil { + Fatalf("Unable to open map: %s", err) + } + + rules := make(map[string][]string) + parse := func(key *crap.CrapKey, val *crap.CrapVal) { + rules[key.String()] = append(rules[key.String()], val.PodIp.String()) + } + + if err := vtep.IterateWithCallback(parse); err != nil { + Fatalf("Error dumping contents of egress policy map: %s\n", err) + } + + if command.OutputOption() { + if err := command.PrintOutput(rules); err != nil { + fmt.Fprintf(os.Stderr, "error getting output of map in %s: %s\n", command.OutputOptionString(), err) + os.Exit(1) + } + return + } + + if len(rules) == 0 { + fmt.Fprintf(os.Stderr, "No entries found.\n") + } else { + TablePrinter(crapRuleCidrTitle, crapRuleTitle, rules) + } + }, +} + +func init() { + BPFCrapCmd.AddCommand(bpfCrapListCmd) + command.AddOutputOption(bpfCrapListCmd) +} diff --git a/cilium-dbg/cmd/bpf_crap_update.go b/cilium-dbg/cmd/bpf_crap_update.go new file mode 100644 index 0000000000000..3a6a095f1822d --- /dev/null +++ b/cilium-dbg/cmd/bpf_crap_update.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "fmt" + "net/netip" + "os" + + "github.com/spf13/cobra" + + "github.com/cilium/cilium/pkg/common" + "github.com/cilium/cilium/pkg/maps/crap" +) + +const ( + crapUpdateUsage = "Create/Update CRAP entry.\n" +) + +var bpfCrapUpdateCmd = &cobra.Command{ + Args: cobra.ExactArgs(2), + Use: "update", + Short: "Update CRAP entries", + Aliases: []string{"add"}, + Long: vtepPolUpdateUsage, + Run: func(cmd *cobra.Command, args []string) { + common.RequireRootPrivilege("cilium bpf crap update ") + + crap, err := crap.OpenPinnedCrapMap(log) + if err != nil { + Fatalf("Unable to open map: %s", err) + } + + dst_ip, err := netip.ParseAddr(args[0]) + if err != nil { + Fatalf("Unable to parse public IP '%s'", args[0]) + } + + pod_ip, err := netip.ParseAddr(args[1]) + if err != nil { + Fatalf("Unable to parse pod IP '%s'", args[1]) + } + + if err := crap.UpdateCrapMapping(dst_ip, pod_ip); err != nil { + fmt.Fprintf(os.Stderr, "error updating contents of map: %s\n", err) + os.Exit(1) + } + }, +} + +func init() { + BPFCrapCmd.AddCommand(bpfCrapUpdateCmd) +} diff --git a/cilium-dbg/cmd/bpf_vtep_policy.go b/cilium-dbg/cmd/bpf_vtep_policy.go new file mode 100644 index 0000000000000..b583461bc68d0 --- /dev/null +++ b/cilium-dbg/cmd/bpf_vtep_policy.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "github.com/spf13/cobra" +) + +// BPFVtepPolicyCmd represents the bpf command +var BPFVtepPolicyCmd = &cobra.Command{ + Use: "vtep-policy", + Short: "Manage the VTEP Policy mappings", +} + +func init() { + BPFCmd.AddCommand(BPFVtepPolicyCmd) +} diff --git a/cilium-dbg/cmd/bpf_vtep_policy_delete.go b/cilium-dbg/cmd/bpf_vtep_policy_delete.go new file mode 100644 index 0000000000000..306dc2e3993b2 --- /dev/null +++ b/cilium-dbg/cmd/bpf_vtep_policy_delete.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "net/netip" + + "github.com/spf13/cobra" + + "github.com/cilium/cilium/pkg/common" + "github.com/cilium/cilium/pkg/maps/vtep_policy" +) + +const ( + vtepPolicyDelUsage = "Delete vtep entries using vtep CIDR.\n" +) + +var bpfVtepPolicyDeleteCmd = &cobra.Command{ + Args: cobra.ExactArgs(2), + Use: "delete", + Short: "Delete VTEP Policy entries", + Long: vtepPolicyDelUsage, + Run: func(cmd *cobra.Command, args []string) { + common.RequireRootPrivilege("cilium bpf vtep-policy delete ") + + vtep, err := vtep_policy.OpenPinnedVtepPolicyMap(log) + if err != nil { + Fatalf("Unable to open map: %s", err) + } + + src_ip, err := netip.ParseAddr(args[0]) + if err != nil { + Fatalf("Unable to parse IP '%s'", args[0]) + } + + dst_cidr, err := netip.ParsePrefix(args[1]) + if err != nil { + Fatalf("error parsing cidr %s: %s", args[1], err) + } + + if err := vtep.RemoveVtepPolicyMapping(src_ip, dst_cidr); err != nil { + Fatalf("error deleting contents of map: %s\n", err) + } + }, +} + +func init() { + BPFVtepPolicyCmd.AddCommand(bpfVtepPolicyDeleteCmd) +} diff --git a/cilium-dbg/cmd/bpf_vtep_policy_list.go b/cilium-dbg/cmd/bpf_vtep_policy_list.go new file mode 100644 index 0000000000000..d3d1dd8b4d734 --- /dev/null +++ b/cilium-dbg/cmd/bpf_vtep_policy_list.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/cilium/cilium/pkg/command" + "github.com/cilium/cilium/pkg/common" + "github.com/cilium/cilium/pkg/maps/vtep_policy" +) + +const ( + vtepPolicyCidrTitle = "SourceIP DestinationCIDR" + vtepPolicyTitle = "VTEP IP/MAC" +) + +var ( + vtepPolicyListUsage = "List VTEP CIDR and their corresponding VTEP MAC/IP.\n" +) + +var bpfVtepPolicyListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List VTEP Policy entries", + Long: vtepPolicyListUsage, + Run: func(cmd *cobra.Command, args []string) { + common.RequireRootPrivilege("cilium bpf vtep-policy list") + + vtep, err := vtep_policy.OpenPinnedVtepPolicyMap(log) + if err != nil { + Fatalf("Unable to open map: %s", err) + } + + bpfVtepList := make(map[string][]string) + parse := func(key *vtep_policy.VtepPolicyKey, val *vtep_policy.VtepPolicyVal) { + bpfVtepList[key.String()] = append(bpfVtepList[key.String()], val.VtepIp.String()) + bpfVtepList[key.String()] = append(bpfVtepList[key.String()], val.Mac.String()) + } + + if err := vtep.IterateWithCallback(parse); err != nil { + Fatalf("Error dumping contents of egress policy map: %s\n", err) + } + + if command.OutputOption() { + if err := command.PrintOutput(bpfVtepList); err != nil { + fmt.Fprintf(os.Stderr, "error getting output of map in %s: %s\n", command.OutputOptionString(), err) + os.Exit(1) + } + return + } + + if len(bpfVtepList) == 0 { + fmt.Fprintf(os.Stderr, "No entries found.\n") + } else { + TablePrinter(vtepPolicyCidrTitle, vtepPolicyTitle, bpfVtepList) + } + }, +} + +func init() { + BPFVtepPolicyCmd.AddCommand(bpfVtepPolicyListCmd) + command.AddOutputOption(bpfVtepPolicyListCmd) +} diff --git a/cilium-dbg/cmd/bpf_vtep_policy_update.go b/cilium-dbg/cmd/bpf_vtep_policy_update.go new file mode 100644 index 0000000000000..f491ae5e310df --- /dev/null +++ b/cilium-dbg/cmd/bpf_vtep_policy_update.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package cmd + +import ( + "fmt" + "net/netip" + "os" + + "github.com/spf13/cobra" + + "github.com/cilium/cilium/pkg/common" + "github.com/cilium/cilium/pkg/mac" + "github.com/cilium/cilium/pkg/maps/vtep_policy" +) + +const ( + vtepPolUpdateUsage = "Create/Update vtep entry.\n" +) + +var bpfVtepPolicyUpdateCmd = &cobra.Command{ + Args: cobra.ExactArgs(4), + Use: "update", + Short: "Update VTEP Policy entries", + Aliases: []string{"add"}, + Long: vtepPolUpdateUsage, + Run: func(cmd *cobra.Command, args []string) { + common.RequireRootPrivilege("cilium bpf vtep-policy update ") + + vtep, err := vtep_policy.OpenPinnedVtepPolicyMap(log) + if err != nil { + Fatalf("Unable to open map: %s", err) + } + + src_ip, err := netip.ParseAddr(args[0]) + if err != nil { + Fatalf("Unable to parse IP '%s'", args[0]) + } + + dst_cidr, err := netip.ParsePrefix(args[1]) + if err != nil { + Fatalf("error parsing cidr %s: %s", args[1], err) + } + + vtep_ip, err := netip.ParseAddr(args[2]) + if err != nil { + Fatalf("Unable to parse IP '%s'", args[2]) + } + + rmac, err := mac.ParseMAC(args[3]) + if err != nil { + Fatalf("Unable to parse vtep mac '%s'", args[3]) + } + + if err := vtep.UpdateVtepPolicyMapping(src_ip, dst_cidr, vtep_ip, rmac); err != nil { + fmt.Fprintf(os.Stderr, "error updating contents of map: %s\n", err) + os.Exit(1) + } + }, +} + +func init() { + BPFVtepPolicyCmd.AddCommand(bpfVtepPolicyUpdateCmd) +} diff --git a/cilium-dbg/cmd/post_uninstall_cleanup.go b/cilium-dbg/cmd/post_uninstall_cleanup.go index c1fb3af82b073..19afd7da88ec5 100644 --- a/cilium-dbg/cmd/post_uninstall_cleanup.go +++ b/cilium-dbg/cmd/post_uninstall_cleanup.go @@ -60,16 +60,15 @@ const ( ) const ( - ciliumLinkPrefix = "cilium_" - ciliumNetNSPrefix = "cilium-" - hostLinkPrefix = "lxc" - hostLinkLen = len(hostLinkPrefix + "XXXXX") - cniPath = "/etc/cni/net.d" - cniConfigV1 = cniPath + "/10-cilium-cni.conf" - cniConfigV2 = cniPath + "/00-cilium-cni.conf" - cniConfigV3 = cniPath + "/05-cilium-cni.conf" - cniConfigV4 = cniPath + "/05-cilium.conf" - cniConfigV5 = cniPath + "/05-cilium.conflist" + ciliumLinkPrefix = "cilium_" + hostLinkPrefix = "lxc" + hostLinkLen = len(hostLinkPrefix + "XXXXX") + cniPath = "/etc/cni/net.d" + cniConfigV1 = cniPath + "/10-cilium-cni.conf" + cniConfigV2 = cniPath + "/00-cilium-cni.conf" + cniConfigV3 = cniPath + "/05-cilium-cni.conf" + cniConfigV4 = cniPath + "/05-cilium.conf" + cniConfigV5 = cniPath + "/05-cilium.conflist" ) func init() { diff --git a/contrib/scripts/k8s-manifests-gen.sh b/contrib/scripts/k8s-manifests-gen.sh index c575ede5afc6d..066fa38a68a77 100755 --- a/contrib/scripts/k8s-manifests-gen.sh +++ b/contrib/scripts/k8s-manifests-gen.sh @@ -39,7 +39,8 @@ CRDS_CILIUM_V2="ciliumnetworkpolicies \ CRDS_CILIUM_V2ALPHA1="ciliumendpointslices \ ciliuml2announcementpolicies \ ciliumpodippools \ - ciliumgatewayclassconfigs" + ciliumgatewayclassconfigs \ + ciliumvteppolicies" TMPDIR=$(mktemp -d -t cilium.tmpXXXXXXXX) go tool sigs.k8s.io/controller-tools/cmd/controller-gen ${CRD_OPTIONS} paths="${CRD_PATHS}" output:crd:artifacts:config="${TMPDIR}" diff --git a/contrib/testing/kind-values.yaml b/contrib/testing/kind-values.yaml index 9be35a51f3fe0..29473b60e2692 100644 --- a/contrib/testing/kind-values.yaml +++ b/contrib/testing/kind-values.yaml @@ -6,3 +6,6 @@ operator: override: "localhost:5000/cilium/operator-generic:local" pullPolicy: Never suffix: "" +vtep: + enabled: true + mask: "255.255.255.0" diff --git a/daemon/cmd/cells.go b/daemon/cmd/cells.go index dbd8c40ab2547..7bf8c23fc4dbc 100644 --- a/daemon/cmd/cells.go +++ b/daemon/cmd/cells.go @@ -79,10 +79,12 @@ import ( policyK8s "github.com/cilium/cilium/pkg/policy/k8s" "github.com/cilium/cilium/pkg/pprof" "github.com/cilium/cilium/pkg/proxy" + "github.com/cilium/cilium/pkg/raw" "github.com/cilium/cilium/pkg/signal" "github.com/cilium/cilium/pkg/source" "github.com/cilium/cilium/pkg/status" "github.com/cilium/cilium/pkg/svcrouteconfig" + "github.com/cilium/cilium/pkg/vteppolicy" "github.com/cilium/cilium/pkg/ztunnel" ) @@ -358,6 +360,12 @@ var ( // Instantiates an xDS server used for zTunnel integration. ztunnel.Cell, + + // VTEP Policy allows two-way communication with an external VXLAN gateway + vteppolicy.Cell, + + // CRAP cell + raw.Cell, ) ) diff --git a/install/kubernetes/cilium/templates/cilium-agent/clusterrole.yaml b/install/kubernetes/cilium/templates/cilium-agent/clusterrole.yaml index 57b13447bf12c..0453711afb08f 100644 --- a/install/kubernetes/cilium/templates/cilium-agent/clusterrole.yaml +++ b/install/kubernetes/cilium/templates/cilium-agent/clusterrole.yaml @@ -111,6 +111,7 @@ rules: - ciliumcidrgroups - ciliuml2announcementpolicies - ciliumpodippools + - ciliumvteppolicies verbs: - list - watch diff --git a/install/kubernetes/cilium/templates/cilium-operator/clusterrole.yaml b/install/kubernetes/cilium/templates/cilium-operator/clusterrole.yaml index 0c2ad93dbec45..c0aae46a9b6a4 100644 --- a/install/kubernetes/cilium/templates/cilium-operator/clusterrole.yaml +++ b/install/kubernetes/cilium/templates/cilium-operator/clusterrole.yaml @@ -261,6 +261,7 @@ rules: - ciliuml2announcementpolicies.cilium.io - ciliumpodippools.cilium.io - ciliumgatewayclassconfigs.cilium.io + - ciliumvteppolicies.cilium.io - apiGroups: - cilium.io resources: diff --git a/install/kubernetes/cilium/templates/cilium-preflight/clusterrole.yaml b/install/kubernetes/cilium/templates/cilium-preflight/clusterrole.yaml index fbf511cd93755..5d3c4667c698b 100644 --- a/install/kubernetes/cilium/templates/cilium-preflight/clusterrole.yaml +++ b/install/kubernetes/cilium/templates/cilium-preflight/clusterrole.yaml @@ -111,6 +111,7 @@ rules: - ciliumcidrgroups - ciliuml2announcementpolicies - ciliumpodippools + - ciliumvteppolicies verbs: - list - watch diff --git a/pkg/annotation/k8s.go b/pkg/annotation/k8s.go index fca37ed95e992..dadef21510deb 100644 --- a/pkg/annotation/k8s.go +++ b/pkg/annotation/k8s.go @@ -156,6 +156,8 @@ const ( // the latter two, one can set the annotation with the value "LoadBalancer". ServiceTypeExposure = ServicePrefix + "/type" + ServiceRaw = ServicePrefix + "/raw" + // ServiceSourceRangesPolicy is the annotation name used to specify the policy // of the user-provided loadBalancerSourceRanges, meaning whether this CIDR // list should act as an allow- or deny-list. Both "allow" or "deny" are diff --git a/pkg/datapath/bpf/probes_bpfeb.o b/pkg/datapath/bpf/probes_bpfeb.o index 50d9bb6738831..a299a04f17952 100644 Binary files a/pkg/datapath/bpf/probes_bpfeb.o and b/pkg/datapath/bpf/probes_bpfeb.o differ diff --git a/pkg/datapath/bpf/probes_bpfel.o b/pkg/datapath/bpf/probes_bpfel.o index 557b6377c934b..53c3bfb6f7dd2 100644 Binary files a/pkg/datapath/bpf/probes_bpfel.o and b/pkg/datapath/bpf/probes_bpfel.o differ diff --git a/pkg/datapath/bpf/sockterm_bpfeb.o b/pkg/datapath/bpf/sockterm_bpfeb.o index a0d4572befef4..8c73e593ea756 100644 Binary files a/pkg/datapath/bpf/sockterm_bpfeb.o and b/pkg/datapath/bpf/sockterm_bpfeb.o differ diff --git a/pkg/datapath/bpf/sockterm_bpfel.o b/pkg/datapath/bpf/sockterm_bpfel.o index ce79edf5eb716..50cbdc31c99e7 100644 Binary files a/pkg/datapath/bpf/sockterm_bpfel.o and b/pkg/datapath/bpf/sockterm_bpfel.o differ diff --git a/pkg/datapath/linux/config/config.go b/pkg/datapath/linux/config/config.go index e6bea2a967f36..d82721ae6de30 100644 --- a/pkg/datapath/linux/config/config.go +++ b/pkg/datapath/linux/config/config.go @@ -47,6 +47,7 @@ import ( "github.com/cilium/cilium/pkg/maps/nodemap" "github.com/cilium/cilium/pkg/maps/policymap" "github.com/cilium/cilium/pkg/maps/vtep" + "github.com/cilium/cilium/pkg/maps/vtep_policy" "github.com/cilium/cilium/pkg/netns" "github.com/cilium/cilium/pkg/option" ) @@ -539,6 +540,7 @@ func (h *HeaderfileWriter) WriteNodeConfig(w io.Writer, cfg *datapath.LocalNodeC } cDefinesMap["VTEP_MAP_SIZE"] = fmt.Sprintf("%d", vtep.MaxEntries) + cDefinesMap["VTEP_POLICY_MAP_SIZE"] = fmt.Sprintf("%d", vtep_policy.MaxEntries) vlanFilter, err := vlanFilterMacros(nativeDevices) if err != nil { diff --git a/pkg/datapath/vtep/cell.go b/pkg/datapath/vtep/cell.go index 5bbeb02cba8ce..c295b6cd24a11 100644 --- a/pkg/datapath/vtep/cell.go +++ b/pkg/datapath/vtep/cell.go @@ -87,10 +87,6 @@ func (r config) Flags(flags *pflag.FlagSet) { func (r config) validatedConfig() (*vtepManagerConfig, error) { config := vtepManagerConfig{} - if len(r.VTEPEndpoint) < 1 { - return nil, fmt.Errorf("If VTEP is enabled, at least one VTEP device must be configured") - } - if len(r.VTEPEndpoint) > defaults.MaxVTEPDevices { return nil, fmt.Errorf("VTEP must not exceed %d VTEP devices (Found %d VTEPs)", defaults.MaxVTEPDevices, len(r.VTEPEndpoint)) } diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index a6d75cfc793ec..67ade6bb8a278 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -483,8 +483,9 @@ const ( TunnelPortGeneve uint16 = 6081 // EnableVTEP enables VXLAN Tunnel Endpoint (VTEP) Integration - EnableVTEP = false - MaxVTEPDevices = 8 + EnableVTEP = false + MaxVTEPDevices = 8 + MaxVtepPolicyEntries = 16384 // Enable BGP control plane features. EnableBGPControlPlane = false diff --git a/pkg/k8s/apis/cilium.io/client/crds/v2alpha1/ciliumvteppolicies.yaml b/pkg/k8s/apis/cilium.io/client/crds/v2alpha1/ciliumvteppolicies.yaml new file mode 100644 index 0000000000000..da2c1714c7ba1 --- /dev/null +++ b/pkg/k8s/apis/cilium.io/client/crds/v2alpha1/ciliumvteppolicies.yaml @@ -0,0 +1,266 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.3 + name: ciliumvteppolicies.cilium.io +spec: + group: cilium.io + names: + categories: + - cilium + - ciliumpolicy + kind: CiliumVtepPolicy + listKind: CiliumVtepPolicyList + plural: ciliumvteppolicies + shortNames: + - vtep-policy + singular: ciliumvteppolicy + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v2alpha1 + schema: + openAPIV3Schema: + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + destinationCIDRs: + description: |- + DestinationCIDRs is a list of destination CIDRs for destination IP addresses. + If a destination IP matches any one CIDR, it will be selected. + items: + format: cidr + type: string + maxItems: 30 + type: array + externalVTEP: + description: ExternalVTEP is the remote VTEP outside Cilium network. + properties: + ip: + description: |- + IP is the VTEP IP (remote node terminating VXLAN tunnel) + + Example: + When set to "192.168.1.100", matching traffic will be + redirected to the VXLAN tunnel towards IP address 192.168.1.100. + format: ipv4 + type: string + mac: + description: |- + MAC is a remote MAC address on the other side of VXLAN tunnel. This is + needed to build l2 and avoid ARP. + + Example: + 00:11:22:33:44:55 that belongs to VXLAN tunnel interface on the remote side + pattern: ^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$ + type: string + type: object + selectors: + description: |- + CiliumVtepPolicyRules represents a list of rules by which traffic is + selected from/to the pods. + items: + properties: + namespaceSelector: + description: |- + Selects Namespaces using cluster-scoped labels. This field follows standard label + selector semantics; if present but empty, it selects all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + enum: + - In + - NotIn + - Exists + - DoesNotExist + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + description: MatchLabelsValue represents the value from + the MatchLabels {key,value} pair. + maxLength: 63 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + nodeSelector: + description: |- + This is a label selector which selects Pods by Node. This field follows standard label + selector semantics; if present but empty, it selects all nodes. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + enum: + - In + - NotIn + - Exists + - DoesNotExist + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + description: MatchLabelsValue represents the value from + the MatchLabels {key,value} pair. + maxLength: 63 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + This is a label selector which selects Pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + enum: + - In + - NotIn + - Exists + - DoesNotExist + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + description: MatchLabelsValue represents the value from + the MatchLabels {key,value} pair. + maxLength: 63 + pattern: ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$ + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + maxItems: 30 + type: array + type: object + required: + - metadata + type: object + served: true + storage: true + subresources: {} diff --git a/pkg/k8s/apis/cilium.io/client/register.go b/pkg/k8s/apis/cilium.io/client/register.go index d414339820814..2db74b00093eb 100644 --- a/pkg/k8s/apis/cilium.io/client/register.go +++ b/pkg/k8s/apis/cilium.io/client/register.go @@ -90,6 +90,9 @@ const ( CPIPCRDName = k8sconstv2alpha1.CPIPKindDefinition + "/" + k8sconstv2alpha1.CustomResourceDefinitionVersion // CGCCCRDName is the full name of the CiliumGatewayClassConfig CRD. CGCCCRDName = k8sconstv2alpha1.CGCCKindDefinition + "/" + k8sconstv2alpha1.CustomResourceDefinitionVersion + + // CVPCRDName is the full name of the CiliumVtepPolicy CRD. + CVPCRDName = k8sconstv2alpha1.CVPKindDefinition + "/" + k8sconstv2alpha1.CustomResourceDefinitionVersion ) type CRDList struct { @@ -184,6 +187,10 @@ func CustomResourceDefinitionList() map[string]*CRDList { Name: CGCCCRDName, FullName: k8sconstv2alpha1.CGCCName, }, + synced.CRDResourceName(k8sconstv2alpha1.CVPName): { + Name: CVPCRDName, + FullName: k8sconstv2alpha1.CVPName, + }, } } @@ -268,6 +275,9 @@ var ( //go:embed crds/v2alpha1/ciliumgatewayclassconfigs.yaml crdsv2Alpha1CiliumGatewayClassConfigs []byte + + //go:embed crds/v2alpha1/ciliumvteppolicies.yaml + crdsv2Alpha1CiliumVtepPolicies []byte ) // GetPregeneratedCRD returns the pregenerated CRD based on the requested CRD @@ -324,6 +334,8 @@ func GetPregeneratedCRD(logger *slog.Logger, crdName string) apiextensionsv1.Cus crdBytes = crdsv2Alpha1CiliumPodIPPools case CGCCCRDName: crdBytes = crdsv2Alpha1CiliumGatewayClassConfigs + case CVPCRDName: + crdBytes = crdsv2Alpha1CiliumVtepPolicies default: logging.Fatal(logger, "Pregenerated CRD does not exist", logfields.CRDName, crdName) } diff --git a/pkg/k8s/apis/cilium.io/v2alpha1/register.go b/pkg/k8s/apis/cilium.io/v2alpha1/register.go index 52f508c4a70c6..dcb21f19ccd96 100644 --- a/pkg/k8s/apis/cilium.io/v2alpha1/register.go +++ b/pkg/k8s/apis/cilium.io/v2alpha1/register.go @@ -100,6 +100,11 @@ const ( CGCCListName = "ciliumgatewayclassconfiglists" CGCCKindDefinition = "CiliumGatewayClassConfig" CGCCName = CGCCPluralName + "." + CustomResourceDefinitionGroup + + // CiliumVtepPolicy + CVPPluralName = "ciliumvteppolicies" + CVPKindDefinition = "CiliumVtepPolicy" + CVPName = CVPPluralName + "." + CustomResourceDefinitionGroup ) // SchemeGroupVersion is group version used to register these objects @@ -169,6 +174,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { // new Gateway API types &CiliumGatewayClassConfig{}, &CiliumGatewayClassConfigList{}, + + // VTEP Policy API types + &CiliumVtepPolicy{}, + &CiliumVtepPolicyList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) diff --git a/pkg/k8s/apis/cilium.io/v2alpha1/vtep_policy_types.go b/pkg/k8s/apis/cilium.io/v2alpha1/vtep_policy_types.go new file mode 100644 index 0000000000000..b99798798ac39 --- /dev/null +++ b/pkg/k8s/apis/cilium.io/v2alpha1/vtep_policy_types.go @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package v2alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + slimv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories={cilium,ciliumpolicy},singular="ciliumvteppolicy",path="ciliumvteppolicies",scope="Cluster",shortName={vtep-policy} +// +kubebuilder:printcolumn:JSONPath=".metadata.creationTimestamp",name="Age",type=date +// +kubebuilder:storageversion + +type CiliumVtepPolicy struct { + // +k8s:openapi-gen=false + // +deepequal-gen=false + metav1.TypeMeta `json:",inline"` + // +k8s:openapi-gen=false + // +deepequal-gen=false + metav1.ObjectMeta `json:"metadata"` + + Spec CiliumVtepPolicySpec `json:"spec,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +k8s:openapi-gen=false +// +deepequal-gen=false + +// CiliumVtepPolicyList is a list of CiliumVtepPolicy objects. +type CiliumVtepPolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + // Items is a list of CiliumVtepPolicy. + Items []CiliumVtepPolicy `json:"items"` +} + +// +kubebuilder:validation:Format=cidr +type CIDR string + +// +kubebuilder:validation:Pattern=`^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$` +// regex source: https://uibakery.io/regex-library/mac-address +type MAC string + +type CiliumVtepPolicySpec struct { + // +kubebuilder:validation:MaxItems=30 + // CiliumVtepPolicyRules represents a list of rules by which traffic is + // selected from/to the pods. + Selectors []CiliumVtepPolicyRules `json:"selectors,omitempty"` + + // +kubebuilder:validation:MaxItems=30 + // DestinationCIDRs is a list of destination CIDRs for destination IP addresses. + // If a destination IP matches any one CIDR, it will be selected. + DestinationCIDRs []CIDR `json:"destinationCIDRs,omitempty"` + + // ExternalVTEP is the remote VTEP outside Cilium network. + ExternalVTEP *ExternalVTEP `json:"externalVTEP,omitempty"` +} + +// External VTEP identifies the node outside cilium network that should act +// as a gateway for traffic matching the vtep policy +type ExternalVTEP struct { + // IP is the VTEP IP (remote node terminating VXLAN tunnel) + // + // Example: + // When set to "192.168.1.100", matching traffic will be + // redirected to the VXLAN tunnel towards IP address 192.168.1.100. + // + // +kubebuilder:validation:Format=ipv4 + IP string `json:"ip,omitempty"` + + // MAC is a remote MAC address on the other side of VXLAN tunnel. This is + // needed to build l2 and avoid ARP. + // + // Example: + // 00:11:22:33:44:55 that belongs to VXLAN tunnel interface on the remote side + MAC MAC `json:"mac,omitempty"` +} + +type CiliumVtepPolicyRules struct { + // Selects Namespaces using cluster-scoped labels. This field follows standard label + // selector semantics; if present but empty, it selects all namespaces. + NamespaceSelector *slimv1.LabelSelector `json:"namespaceSelector,omitempty"` + + // This is a label selector which selects Pods. This field follows standard label + // selector semantics; if present but empty, it selects all pods. + PodSelector *slimv1.LabelSelector `json:"podSelector,omitempty"` + + // This is a label selector which selects Pods by Node. This field follows standard label + // selector semantics; if present but empty, it selects all nodes. + NodeSelector *slimv1.LabelSelector `json:"nodeSelector,omitempty"` +} diff --git a/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepcopy.go b/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepcopy.go index f03d47110de8d..ea67ac965b108 100644 --- a/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepcopy.go @@ -1748,6 +1748,130 @@ func (in *CiliumPodIPPoolList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CiliumVtepPolicy) DeepCopyInto(out *CiliumVtepPolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CiliumVtepPolicy. +func (in *CiliumVtepPolicy) DeepCopy() *CiliumVtepPolicy { + if in == nil { + return nil + } + out := new(CiliumVtepPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CiliumVtepPolicy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CiliumVtepPolicyList) DeepCopyInto(out *CiliumVtepPolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CiliumVtepPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CiliumVtepPolicyList. +func (in *CiliumVtepPolicyList) DeepCopy() *CiliumVtepPolicyList { + if in == nil { + return nil + } + out := new(CiliumVtepPolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CiliumVtepPolicyList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CiliumVtepPolicyRules) DeepCopyInto(out *CiliumVtepPolicyRules) { + *out = *in + if in.NamespaceSelector != nil { + in, out := &in.NamespaceSelector, &out.NamespaceSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.PodSelector != nil { + in, out := &in.PodSelector, &out.PodSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CiliumVtepPolicyRules. +func (in *CiliumVtepPolicyRules) DeepCopy() *CiliumVtepPolicyRules { + if in == nil { + return nil + } + out := new(CiliumVtepPolicyRules) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CiliumVtepPolicySpec) DeepCopyInto(out *CiliumVtepPolicySpec) { + *out = *in + if in.Selectors != nil { + in, out := &in.Selectors, &out.Selectors + *out = make([]CiliumVtepPolicyRules, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DestinationCIDRs != nil { + in, out := &in.DestinationCIDRs, &out.DestinationCIDRs + *out = make([]CIDR, len(*in)) + copy(*out, *in) + } + if in.ExternalVTEP != nil { + in, out := &in.ExternalVTEP, &out.ExternalVTEP + *out = new(ExternalVTEP) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CiliumVtepPolicySpec. +func (in *CiliumVtepPolicySpec) DeepCopy() *CiliumVtepPolicySpec { + if in == nil { + return nil + } + out := new(CiliumVtepPolicySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CoreCiliumEndpoint) DeepCopyInto(out *CoreCiliumEndpoint) { *out = *in @@ -1807,6 +1931,22 @@ func (in *EgressRule) DeepCopy() *EgressRule { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalVTEP) DeepCopyInto(out *ExternalVTEP) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalVTEP. +func (in *ExternalVTEP) DeepCopy() *ExternalVTEP { + if in == nil { + return nil + } + out := new(ExternalVTEP) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPPoolSpec) DeepCopyInto(out *IPPoolSpec) { *out = *in diff --git a/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepequal.go b/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepequal.go index fac460f5e9e65..de80bab51039f 100644 --- a/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepequal.go +++ b/pkg/k8s/apis/cilium.io/v2alpha1/zz_generated.deepequal.go @@ -1283,6 +1283,106 @@ func (in *CiliumPodIPPool) DeepEqual(other *CiliumPodIPPool) bool { return true } +// DeepEqual is an autogenerated deepequal function, deeply comparing the +// receiver with other. in must be non-nil. +func (in *CiliumVtepPolicy) DeepEqual(other *CiliumVtepPolicy) bool { + if other == nil { + return false + } + + if !in.Spec.DeepEqual(&other.Spec) { + return false + } + + return true +} + +// DeepEqual is an autogenerated deepequal function, deeply comparing the +// receiver with other. in must be non-nil. +func (in *CiliumVtepPolicyRules) DeepEqual(other *CiliumVtepPolicyRules) bool { + if other == nil { + return false + } + + if (in.NamespaceSelector == nil) != (other.NamespaceSelector == nil) { + return false + } else if in.NamespaceSelector != nil { + if !in.NamespaceSelector.DeepEqual(other.NamespaceSelector) { + return false + } + } + + if (in.PodSelector == nil) != (other.PodSelector == nil) { + return false + } else if in.PodSelector != nil { + if !in.PodSelector.DeepEqual(other.PodSelector) { + return false + } + } + + if (in.NodeSelector == nil) != (other.NodeSelector == nil) { + return false + } else if in.NodeSelector != nil { + if !in.NodeSelector.DeepEqual(other.NodeSelector) { + return false + } + } + + return true +} + +// DeepEqual is an autogenerated deepequal function, deeply comparing the +// receiver with other. in must be non-nil. +func (in *CiliumVtepPolicySpec) DeepEqual(other *CiliumVtepPolicySpec) bool { + if other == nil { + return false + } + + if ((in.Selectors != nil) && (other.Selectors != nil)) || ((in.Selectors == nil) != (other.Selectors == nil)) { + in, other := &in.Selectors, &other.Selectors + if other == nil { + return false + } + + if len(*in) != len(*other) { + return false + } else { + for i, inElement := range *in { + if !inElement.DeepEqual(&(*other)[i]) { + return false + } + } + } + } + + if ((in.DestinationCIDRs != nil) && (other.DestinationCIDRs != nil)) || ((in.DestinationCIDRs == nil) != (other.DestinationCIDRs == nil)) { + in, other := &in.DestinationCIDRs, &other.DestinationCIDRs + if other == nil { + return false + } + + if len(*in) != len(*other) { + return false + } else { + for i, inElement := range *in { + if inElement != (*other)[i] { + return false + } + } + } + } + + if (in.ExternalVTEP == nil) != (other.ExternalVTEP == nil) { + return false + } else if in.ExternalVTEP != nil { + if !in.ExternalVTEP.DeepEqual(other.ExternalVTEP) { + return false + } + } + + return true +} + // DeepEqual is an autogenerated deepequal function, deeply comparing the // receiver with other. in must be non-nil. func (in *CoreCiliumEndpoint) DeepEqual(other *CoreCiliumEndpoint) bool { @@ -1348,6 +1448,23 @@ func (in *EgressRule) DeepEqual(other *EgressRule) bool { return true } +// DeepEqual is an autogenerated deepequal function, deeply comparing the +// receiver with other. in must be non-nil. +func (in *ExternalVTEP) DeepEqual(other *ExternalVTEP) bool { + if other == nil { + return false + } + + if in.IP != other.IP { + return false + } + if in.MAC != other.MAC { + return false + } + + return true +} + // DeepEqual is an autogenerated deepequal function, deeply comparing the // receiver with other. in must be non-nil. func (in *IPPoolSpec) DeepEqual(other *IPPoolSpec) bool { diff --git a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/cilium.io_client.go b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/cilium.io_client.go index 39be60bea5eaf..43e9ce692a2ab 100644 --- a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/cilium.io_client.go +++ b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/cilium.io_client.go @@ -27,6 +27,7 @@ type CiliumV2alpha1Interface interface { CiliumLoadBalancerIPPoolsGetter CiliumNodeConfigsGetter CiliumPodIPPoolsGetter + CiliumVtepPoliciesGetter } // CiliumV2alpha1Client is used to interact with features provided by the cilium.io group. @@ -82,6 +83,10 @@ func (c *CiliumV2alpha1Client) CiliumPodIPPools() CiliumPodIPPoolInterface { return newCiliumPodIPPools(c) } +func (c *CiliumV2alpha1Client) CiliumVtepPolicies() CiliumVtepPolicyInterface { + return newCiliumVtepPolicies(c) +} + // NewForConfig creates a new CiliumV2alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/ciliumvteppolicy.go b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/ciliumvteppolicy.go new file mode 100644 index 0000000000000..9f588827b16b7 --- /dev/null +++ b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/ciliumvteppolicy.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// Code generated by client-gen. DO NOT EDIT. + +package v2alpha1 + +import ( + context "context" + + ciliumiov2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + scheme "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// CiliumVtepPoliciesGetter has a method to return a CiliumVtepPolicyInterface. +// A group's client should implement this interface. +type CiliumVtepPoliciesGetter interface { + CiliumVtepPolicies() CiliumVtepPolicyInterface +} + +// CiliumVtepPolicyInterface has methods to work with CiliumVtepPolicy resources. +type CiliumVtepPolicyInterface interface { + Create(ctx context.Context, ciliumVtepPolicy *ciliumiov2alpha1.CiliumVtepPolicy, opts v1.CreateOptions) (*ciliumiov2alpha1.CiliumVtepPolicy, error) + Update(ctx context.Context, ciliumVtepPolicy *ciliumiov2alpha1.CiliumVtepPolicy, opts v1.UpdateOptions) (*ciliumiov2alpha1.CiliumVtepPolicy, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*ciliumiov2alpha1.CiliumVtepPolicy, error) + List(ctx context.Context, opts v1.ListOptions) (*ciliumiov2alpha1.CiliumVtepPolicyList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *ciliumiov2alpha1.CiliumVtepPolicy, err error) + CiliumVtepPolicyExpansion +} + +// ciliumVtepPolicies implements CiliumVtepPolicyInterface +type ciliumVtepPolicies struct { + *gentype.ClientWithList[*ciliumiov2alpha1.CiliumVtepPolicy, *ciliumiov2alpha1.CiliumVtepPolicyList] +} + +// newCiliumVtepPolicies returns a CiliumVtepPolicies +func newCiliumVtepPolicies(c *CiliumV2alpha1Client) *ciliumVtepPolicies { + return &ciliumVtepPolicies{ + gentype.NewClientWithList[*ciliumiov2alpha1.CiliumVtepPolicy, *ciliumiov2alpha1.CiliumVtepPolicyList]( + "ciliumvteppolicies", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *ciliumiov2alpha1.CiliumVtepPolicy { return &ciliumiov2alpha1.CiliumVtepPolicy{} }, + func() *ciliumiov2alpha1.CiliumVtepPolicyList { return &ciliumiov2alpha1.CiliumVtepPolicyList{} }, + ), + } +} diff --git a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_cilium.io_client.go b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_cilium.io_client.go index 6bab4e42cdd7d..fbfc8c27e9d9b 100644 --- a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_cilium.io_client.go +++ b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_cilium.io_client.go @@ -63,6 +63,10 @@ func (c *FakeCiliumV2alpha1) CiliumPodIPPools() v2alpha1.CiliumPodIPPoolInterfac return newFakeCiliumPodIPPools(c) } +func (c *FakeCiliumV2alpha1) CiliumVtepPolicies() v2alpha1.CiliumVtepPolicyInterface { + return newFakeCiliumVtepPolicies(c) +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeCiliumV2alpha1) RESTClient() rest.Interface { diff --git a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_ciliumvteppolicy.go b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_ciliumvteppolicy.go new file mode 100644 index 0000000000000..a544adc09165c --- /dev/null +++ b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/fake/fake_ciliumvteppolicy.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + ciliumiov2alpha1 "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeCiliumVtepPolicies implements CiliumVtepPolicyInterface +type fakeCiliumVtepPolicies struct { + *gentype.FakeClientWithList[*v2alpha1.CiliumVtepPolicy, *v2alpha1.CiliumVtepPolicyList] + Fake *FakeCiliumV2alpha1 +} + +func newFakeCiliumVtepPolicies(fake *FakeCiliumV2alpha1) ciliumiov2alpha1.CiliumVtepPolicyInterface { + return &fakeCiliumVtepPolicies{ + gentype.NewFakeClientWithList[*v2alpha1.CiliumVtepPolicy, *v2alpha1.CiliumVtepPolicyList]( + fake.Fake, + "", + v2alpha1.SchemeGroupVersion.WithResource("ciliumvteppolicies"), + v2alpha1.SchemeGroupVersion.WithKind("CiliumVtepPolicy"), + func() *v2alpha1.CiliumVtepPolicy { return &v2alpha1.CiliumVtepPolicy{} }, + func() *v2alpha1.CiliumVtepPolicyList { return &v2alpha1.CiliumVtepPolicyList{} }, + func(dst, src *v2alpha1.CiliumVtepPolicyList) { dst.ListMeta = src.ListMeta }, + func(list *v2alpha1.CiliumVtepPolicyList) []*v2alpha1.CiliumVtepPolicy { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v2alpha1.CiliumVtepPolicyList, items []*v2alpha1.CiliumVtepPolicy) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/generated_expansion.go b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/generated_expansion.go index 445ffcc260a06..fcd071294a56f 100644 --- a/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/generated_expansion.go +++ b/pkg/k8s/client/clientset/versioned/typed/cilium.io/v2alpha1/generated_expansion.go @@ -28,3 +28,5 @@ type CiliumLoadBalancerIPPoolExpansion interface{} type CiliumNodeConfigExpansion interface{} type CiliumPodIPPoolExpansion interface{} + +type CiliumVtepPolicyExpansion interface{} diff --git a/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/ciliumvteppolicy.go b/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/ciliumvteppolicy.go new file mode 100644 index 0000000000000..b05aba8144792 --- /dev/null +++ b/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/ciliumvteppolicy.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// Code generated by informer-gen. DO NOT EDIT. + +package v2alpha1 + +import ( + context "context" + time "time" + + apisciliumiov2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + versioned "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned" + internalinterfaces "github.com/cilium/cilium/pkg/k8s/client/informers/externalversions/internalinterfaces" + ciliumiov2alpha1 "github.com/cilium/cilium/pkg/k8s/client/listers/cilium.io/v2alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// CiliumVtepPolicyInformer provides access to a shared informer and lister for +// CiliumVtepPolicies. +type CiliumVtepPolicyInformer interface { + Informer() cache.SharedIndexInformer + Lister() ciliumiov2alpha1.CiliumVtepPolicyLister +} + +type ciliumVtepPolicyInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewCiliumVtepPolicyInformer constructs a new informer for CiliumVtepPolicy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewCiliumVtepPolicyInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredCiliumVtepPolicyInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredCiliumVtepPolicyInformer constructs a new informer for CiliumVtepPolicy type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredCiliumVtepPolicyInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.CiliumV2alpha1().CiliumVtepPolicies().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.CiliumV2alpha1().CiliumVtepPolicies().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.CiliumV2alpha1().CiliumVtepPolicies().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.CiliumV2alpha1().CiliumVtepPolicies().Watch(ctx, options) + }, + }, + &apisciliumiov2alpha1.CiliumVtepPolicy{}, + resyncPeriod, + indexers, + ) +} + +func (f *ciliumVtepPolicyInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredCiliumVtepPolicyInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *ciliumVtepPolicyInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apisciliumiov2alpha1.CiliumVtepPolicy{}, f.defaultInformer) +} + +func (f *ciliumVtepPolicyInformer) Lister() ciliumiov2alpha1.CiliumVtepPolicyLister { + return ciliumiov2alpha1.NewCiliumVtepPolicyLister(f.Informer().GetIndexer()) +} diff --git a/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/interface.go b/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/interface.go index 393bf884ca11a..7f3f529836848 100644 --- a/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/interface.go +++ b/pkg/k8s/client/informers/externalversions/cilium.io/v2alpha1/interface.go @@ -35,6 +35,8 @@ type Interface interface { CiliumNodeConfigs() CiliumNodeConfigInformer // CiliumPodIPPools returns a CiliumPodIPPoolInformer. CiliumPodIPPools() CiliumPodIPPoolInformer + // CiliumVtepPolicies returns a CiliumVtepPolicyInformer. + CiliumVtepPolicies() CiliumVtepPolicyInformer } type version struct { @@ -107,3 +109,8 @@ func (v *version) CiliumNodeConfigs() CiliumNodeConfigInformer { func (v *version) CiliumPodIPPools() CiliumPodIPPoolInformer { return &ciliumPodIPPoolInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } + +// CiliumVtepPolicies returns a CiliumVtepPolicyInformer. +func (v *version) CiliumVtepPolicies() CiliumVtepPolicyInformer { + return &ciliumVtepPolicyInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/k8s/client/informers/externalversions/generic.go b/pkg/k8s/client/informers/externalversions/generic.go index 59c11c98aff59..f153194bfb8e3 100644 --- a/pkg/k8s/client/informers/externalversions/generic.go +++ b/pkg/k8s/client/informers/externalversions/generic.go @@ -101,6 +101,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Cilium().V2alpha1().CiliumNodeConfigs().Informer()}, nil case v2alpha1.SchemeGroupVersion.WithResource("ciliumpodippools"): return &genericInformer{resource: resource.GroupResource(), informer: f.Cilium().V2alpha1().CiliumPodIPPools().Informer()}, nil + case v2alpha1.SchemeGroupVersion.WithResource("ciliumvteppolicies"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Cilium().V2alpha1().CiliumVtepPolicies().Informer()}, nil } diff --git a/pkg/k8s/client/listers/cilium.io/v2alpha1/ciliumvteppolicy.go b/pkg/k8s/client/listers/cilium.io/v2alpha1/ciliumvteppolicy.go new file mode 100644 index 0000000000000..325977432416a --- /dev/null +++ b/pkg/k8s/client/listers/cilium.io/v2alpha1/ciliumvteppolicy.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// Code generated by lister-gen. DO NOT EDIT. + +package v2alpha1 + +import ( + ciliumiov2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// CiliumVtepPolicyLister helps list CiliumVtepPolicies. +// All objects returned here must be treated as read-only. +type CiliumVtepPolicyLister interface { + // List lists all CiliumVtepPolicies in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*ciliumiov2alpha1.CiliumVtepPolicy, err error) + // Get retrieves the CiliumVtepPolicy from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*ciliumiov2alpha1.CiliumVtepPolicy, error) + CiliumVtepPolicyListerExpansion +} + +// ciliumVtepPolicyLister implements the CiliumVtepPolicyLister interface. +type ciliumVtepPolicyLister struct { + listers.ResourceIndexer[*ciliumiov2alpha1.CiliumVtepPolicy] +} + +// NewCiliumVtepPolicyLister returns a new CiliumVtepPolicyLister. +func NewCiliumVtepPolicyLister(indexer cache.Indexer) CiliumVtepPolicyLister { + return &ciliumVtepPolicyLister{listers.New[*ciliumiov2alpha1.CiliumVtepPolicy](indexer, ciliumiov2alpha1.Resource("ciliumvteppolicy"))} +} diff --git a/pkg/k8s/client/listers/cilium.io/v2alpha1/expansion_generated.go b/pkg/k8s/client/listers/cilium.io/v2alpha1/expansion_generated.go index f5258849f3739..6e053d54d88b3 100644 --- a/pkg/k8s/client/listers/cilium.io/v2alpha1/expansion_generated.go +++ b/pkg/k8s/client/listers/cilium.io/v2alpha1/expansion_generated.go @@ -60,3 +60,7 @@ type CiliumNodeConfigNamespaceListerExpansion interface{} // CiliumPodIPPoolListerExpansion allows custom methods to be added to // CiliumPodIPPoolLister. type CiliumPodIPPoolListerExpansion interface{} + +// CiliumVtepPolicyListerExpansion allows custom methods to be added to +// CiliumVtepPolicyLister. +type CiliumVtepPolicyListerExpansion interface{} diff --git a/pkg/k8s/synced/crd.go b/pkg/k8s/synced/crd.go index bb74cf8f5f4bf..4ed5724583ce1 100644 --- a/pkg/k8s/synced/crd.go +++ b/pkg/k8s/synced/crd.go @@ -92,6 +92,10 @@ func agentCRDResourceNames() []string { CRDResourceName(v2alpha1.L2AnnouncementName), ) + if option.Config.EnableVTEP { + result = append(result, CRDResourceName(v2alpha1.CVPName)) + } + return result } diff --git a/pkg/k8s/watchers/watcher.go b/pkg/k8s/watchers/watcher.go index 43d8cb6d12017..a4c56546db6f2 100644 --- a/pkg/k8s/watchers/watcher.go +++ b/pkg/k8s/watchers/watcher.go @@ -204,6 +204,7 @@ var ciliumResourceToGroupMapping = map[string]watcherInfo{ synced.CRDResourceName(cilium_v2.CCGName): {waitOnly, k8sAPIGroupCiliumCIDRGroupV2}, synced.CRDResourceName(v2alpha1.L2AnnouncementName): {skip, ""}, // Handled by L2 announcement directly synced.CRDResourceName(v2alpha1.CPIPName): {skip, ""}, // Handled by multi-pool IPAM allocator + synced.CRDResourceName(v2alpha1.CVPName): {skip, ""}, // Handled by vtep policy manager } func GetGroupsForCiliumResources(logger *slog.Logger, ciliumResources []string) ([]string, []string) { diff --git a/pkg/loadbalancer/reflectors/conversions.go b/pkg/loadbalancer/reflectors/conversions.go index 60e257101c6ed..1324f56c00a8a 100644 --- a/pkg/loadbalancer/reflectors/conversions.go +++ b/pkg/loadbalancer/reflectors/conversions.go @@ -75,6 +75,12 @@ func convertService(cfg loadbalancer.Config, extCfg loadbalancer.ExternalConfig, ) }) + if _, ok := annotation.Get(svc, annotation.ServiceRaw); ok { + log().Warn("Ignoring annotation raw services", + logfields.Annotations, annotation.ServiceRaw, + ) + } + name := loadbalancer.NewServiceName(svc.Namespace, svc.Name) s = &loadbalancer.Service{ Name: name, diff --git a/pkg/logging/logfields/logfields.go b/pkg/logging/logfields/logfields.go index 2ea79decc6621..9b3ee2ac213fa 100644 --- a/pkg/logging/logfields/logfields.go +++ b/pkg/logging/logfields/logfields.go @@ -428,6 +428,9 @@ const ( // CiliumEgressGatewayPolicyName is the name of a CiliumEgressGatewayPolicy CiliumEgressGatewayPolicyName = "ciliumEgressGatewayPolicyName" + // CiliumVtepPolicyName is the name of a CiliumVtepPolicy + CiliumVtepPolicyName = "ciliumVtepPolicyName" + // CiliumClusterwideEnvoyConfigName is the name of a CiliumClusterwideEnvoyConfig CiliumClusterwideEnvoyConfigName = "ciliumClusterwideEnvoyConfigName" @@ -845,6 +848,12 @@ const ( // GatewayIP is the gateway IP used in a given egress policy GatewayIP = "gatewayIP" + // VtepIP is the ip address of remote Vxlan tunnel Endpoint + VtepIP = "vtepIP" + + // VtepMAC is the mac address of remote vxlan tunnel Endpoint + VtepMAC = "vtepMAC" + // Number of Backends failed while restoration. RestoredBackends = "restoredBackends" diff --git a/pkg/maps/cells.go b/pkg/maps/cells.go index a3adaad2a2237..acc0f4ac09333 100644 --- a/pkg/maps/cells.go +++ b/pkg/maps/cells.go @@ -13,6 +13,7 @@ import ( "github.com/cilium/cilium/pkg/maps/authmap" "github.com/cilium/cilium/pkg/maps/bwmap" "github.com/cilium/cilium/pkg/maps/configmap" + "github.com/cilium/cilium/pkg/maps/crap" "github.com/cilium/cilium/pkg/maps/ctmap" "github.com/cilium/cilium/pkg/maps/ctmap/gc" "github.com/cilium/cilium/pkg/maps/egressmap" @@ -29,6 +30,7 @@ import ( "github.com/cilium/cilium/pkg/maps/signalmap" "github.com/cilium/cilium/pkg/maps/srv6map" "github.com/cilium/cilium/pkg/maps/vtep" + "github.com/cilium/cilium/pkg/maps/vtep_policy" ) // Cell contains all cells which are providing BPF Maps. @@ -97,6 +99,12 @@ var Cell = cell.Module( // Provides access to the vtep map. vtep.Cell, + + // Provides access to vtep policy maps + vtep_policy.Cell, + + // CRAP provides direct public access + crap.Cell, ) type mapApiHandlerOut struct { diff --git a/pkg/maps/crap/cell.go b/pkg/maps/crap/cell.go new file mode 100644 index 0000000000000..b944f7e30911a --- /dev/null +++ b/pkg/maps/crap/cell.go @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package crap + +import "github.com/cilium/hive/cell" + +var Cell = cell.Module( + "crap", + "CRAP provides direct public access", + cell.Provide(createPolicyMapFromDaemonConfig), +) diff --git a/pkg/maps/crap/crap.go b/pkg/maps/crap/crap.go new file mode 100644 index 0000000000000..9ccba0c777b37 --- /dev/null +++ b/pkg/maps/crap/crap.go @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package crap + +import ( + "fmt" + "net/netip" + + "log/slog" + + "github.com/cilium/cilium/pkg/bpf" + "github.com/cilium/cilium/pkg/ebpf" + "github.com/cilium/cilium/pkg/metrics" + "github.com/cilium/cilium/pkg/option" + "github.com/cilium/cilium/pkg/types" + "github.com/cilium/hive/cell" +) + +const ( + MaxEntries = 8192 + CrapMapName = "cilium_crap_map" +) + +// Must be in sync with struct crap_key in +type CrapKey struct { + DestIP types.IPv4 `align:"dst_ip"` +} + +func (k CrapKey) String() string { + return fmt.Sprintf("%s", k.DestIP) +} + +func (k *CrapKey) New() bpf.MapKey { return &CrapKey{} } + +// NewKey returns an Key based on the provided source IP address and destination CIDR +func NewKey(dstIP netip.Addr) CrapKey { + result := CrapKey{} + + ip4 := dstIP.As4() + copy(result.DestIP[:], ip4[:]) + + return result +} + +// CrapVal implements the bpf.MapValue interface. It contains the +type CrapVal struct { + PodIp types.IPv4 `align:"pod_ip"` +} + +func (v *CrapVal) String() string { + return fmt.Sprintf("pod_ip=%s", v.PodIp) +} + +func (v *CrapVal) New() bpf.MapValue { return &CrapVal{} } + +// Map represents an CRAP BPF map. +type CrapMap struct { + m *bpf.Map +} + +func createPolicyMapFromDaemonConfig(lifecycle cell.Lifecycle, cfg *option.DaemonConfig, metricsRegistry *metrics.Registry) bpf.MapOut[*CrapMap] { + if !cfg.EnableIPv4 { + return bpf.NewMapOut[*CrapMap](nil) + } + + return bpf.NewMapOut(newCrapMap(lifecycle, metricsRegistry, ebpf.PinByName)) +} + +// CreatePrivatePolicyMap4 creates an unpinned IPv4 policy map. +// +// Useful for testing. +func CreatePrivatePolicyMap(lc cell.Lifecycle, registry *metrics.Registry) *CrapMap { + return newCrapMap(lc, registry, ebpf.PinNone) +} + +func newCrapMap(lc cell.Lifecycle, registry *metrics.Registry, pinning ebpf.PinType) *CrapMap { + m := bpf.NewMap( + CrapMapName, + ebpf.Hash, + &CrapKey{}, + &CrapVal{}, + 8192, + 0, + ).WithCache().WithPressureMetric(registry). + WithEvents(option.Config.GetEventBufferConfig(CrapMapName)) + + lc.Append(cell.Hook{ + OnStart: func(cell.HookContext) error { + switch pinning { + case ebpf.PinNone: + return m.CreateUnpinned() + case ebpf.PinByName: + return m.OpenOrCreate() + } + return fmt.Errorf("received unexpected pin type: %d", pinning) + }, + OnStop: func(cell.HookContext) error { + return m.Close() + }, + }) + + return &CrapMap{m} +} + +func NewVal(newTunnelEndpoint netip.Addr) CrapVal { + value := CrapVal{} + + ip4 := newTunnelEndpoint.As4() + copy(value.PodIp[:], ip4[:]) + + return value +} + +// OpenPinnedCrapMap opens an existing pinned IPv4 policy map. +func OpenPinnedCrapMap(logger *slog.Logger) (*CrapMap, error) { + m, err := bpf.OpenMap(bpf.MapPath(logger, CrapMapName), &CrapKey{}, &CrapVal{}) + if err != nil { + return nil, err + } + + return &CrapMap{m}, nil +} + +func (m *CrapMap) UpdateCrapMapping(dstIP netip.Addr, podIp netip.Addr) error { + key := NewKey(dstIP) + value := NewVal(podIp) + + return m.m.Update(&key, &value) +} + +func (m *CrapMap) Update(key CrapKey, value CrapVal) error { + return m.m.Update(&key, &value) +} + +func (m *CrapMap) RemoveCrapMapping(dstIP netip.Addr) error { + key := NewKey(dstIP) + return m.m.Delete(&key) +} + +func (m *CrapMap) Delete(key *CrapKey) error { + return m.m.Delete(key) +} + +func (m *CrapMap) Lookup(key *CrapKey) (*CrapVal, error) { + ret, err := m.m.Lookup(key) + if err != nil { + return nil, err + } + return ret.(*CrapVal), err +} + +// CrapIterateCallback represents the signature of the callback function +// expected by the IterateWithCallback method, which in turn is used to iterate +// all the keys/values of an crap bpf map. +type CrapIterateCallback func(*CrapKey, *CrapVal) + +// IterateWithCallback iterates through all the keys/values of crap rules +// map, passing each key/value pair to the cb callback. +func (m *CrapMap) IterateWithCallback(cb CrapIterateCallback) error { + return m.m.DumpWithCallback(func(k bpf.MapKey, v bpf.MapValue) { + key := k.(*CrapKey) + value := v.(*CrapVal) + + cb(key, value) + }) +} + +func (k *CrapKey) Match(dst_ip netip.Addr) bool { + nkey := NewKey(dst_ip) + return nkey == *k +} + +func (v *CrapVal) Match(podIP netip.Addr) bool { + nval := NewVal(podIP) + return nval == *v +} diff --git a/pkg/maps/crap/doc.go b/pkg/maps/crap/doc.go new file mode 100644 index 0000000000000..626571465fd8d --- /dev/null +++ b/pkg/maps/crap/doc.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// +groupName=maps +package crap diff --git a/pkg/maps/vtep_policy/doc.go b/pkg/maps/vtep_policy/doc.go new file mode 100644 index 0000000000000..45aab44426879 --- /dev/null +++ b/pkg/maps/vtep_policy/doc.go @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// +groupName=maps +package vtep_policy diff --git a/pkg/maps/vtep_policy/policy.go b/pkg/maps/vtep_policy/policy.go new file mode 100644 index 0000000000000..78eee7dc046b9 --- /dev/null +++ b/pkg/maps/vtep_policy/policy.go @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vtep_policy + +import "github.com/cilium/hive/cell" + +var Cell = cell.Module( + "vteppolicy", + "VTEP policy provide access to the egress gateway datapath maps", + cell.Provide(createPolicyMapFromDaemonConfig), +) diff --git a/pkg/maps/vtep_policy/vtep.go b/pkg/maps/vtep_policy/vtep.go new file mode 100644 index 0000000000000..37ba5c5b7e72c --- /dev/null +++ b/pkg/maps/vtep_policy/vtep.go @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vtep_policy + +import ( + "fmt" + "net/netip" + + "log/slog" + + "github.com/cilium/cilium/pkg/bpf" + "github.com/cilium/cilium/pkg/defaults" + "github.com/cilium/cilium/pkg/ebpf" + "github.com/cilium/cilium/pkg/mac" + "github.com/cilium/cilium/pkg/metrics" + "github.com/cilium/cilium/pkg/option" + "github.com/cilium/cilium/pkg/types" + "github.com/cilium/hive/cell" +) + +const ( + MaxEntries = 16384 + // Name is the canonical name for the VTEP map on the filesystem. + VtepPolicyMapName = "cilium_vtep_policy_map" +) + +// Must be in sync with struct vtep_key in +type VtepPolicyKey struct { + PrefixLen uint32 `align:"prefixlen"` + SourceIP types.IPv4 `align:"src_ip"` + DestCIDR types.IPv4 `align:"dst_ip"` +} + +func (k VtepPolicyKey) String() string { + return fmt.Sprintf("%s.%s/%d", k.SourceIP, k.DestCIDR, k.PrefixLen) +} + +func (k *VtepPolicyKey) New() bpf.MapKey { return &VtepPolicyKey{} } + +// NewKey returns an Key based on the provided source IP address and destination CIDR +func NewKey(srcIP netip.Addr, dstCIDR netip.Prefix) VtepPolicyKey { + result := VtepPolicyKey{} + + ip4 := srcIP.As4() + copy(result.SourceIP[:], ip4[:]) + + cidr := dstCIDR.Addr().As4() + copy(result.DestCIDR[:], cidr[:]) + + result.PrefixLen = 32 + uint32(dstCIDR.Bits()) + + return result +} + +// VtepPolicyVal implements the bpf.MapValue interface. It contains the +// VTEP endpoint MAC and IP +type VtepPolicyVal struct { + Mac mac.Uint64MAC `align:"vtep_mac"` + VtepIp types.IPv4 `align:"tunnel_endpoint"` + _ [4]byte +} + +func (v *VtepPolicyVal) String() string { + return fmt.Sprintf("vtepmac=%s tunnelendpoint=%s", + v.Mac, v.VtepIp) +} + +func (v *VtepPolicyVal) New() bpf.MapValue { return &VtepPolicyVal{} } + +// Map represents an VTEP BPF map. +type VtepPolicyMap struct { + m *bpf.Map +} + +func createPolicyMapFromDaemonConfig(lifecycle cell.Lifecycle, cfg *option.DaemonConfig, metricsRegistry *metrics.Registry) bpf.MapOut[*VtepPolicyMap] { + if !cfg.EnableVTEP || !cfg.EnableIPv4 { + return bpf.NewMapOut[*VtepPolicyMap](nil) + } + + return bpf.NewMapOut(newVtepPolicyMap(lifecycle, metricsRegistry, ebpf.PinByName)) +} + +// CreatePrivatePolicyMap4 creates an unpinned IPv4 policy map. +// +// Useful for testing. +func CreatePrivatePolicyMap(lc cell.Lifecycle, registry *metrics.Registry) *VtepPolicyMap { + return newVtepPolicyMap(lc, registry, ebpf.PinNone) +} + +func newVtepPolicyMap(lc cell.Lifecycle, registry *metrics.Registry, pinning ebpf.PinType) *VtepPolicyMap { + m := bpf.NewMap( + VtepPolicyMapName, + ebpf.LPMTrie, + &VtepPolicyKey{}, + &VtepPolicyVal{}, + defaults.MaxVtepPolicyEntries, + 0, + ).WithCache().WithPressureMetric(registry). + WithEvents(option.Config.GetEventBufferConfig(VtepPolicyMapName)) + + lc.Append(cell.Hook{ + OnStart: func(cell.HookContext) error { + switch pinning { + case ebpf.PinNone: + return m.CreateUnpinned() + case ebpf.PinByName: + return m.OpenOrCreate() + } + return fmt.Errorf("received unexpected pin type: %d", pinning) + }, + OnStop: func(cell.HookContext) error { + return m.Close() + }, + }) + + return &VtepPolicyMap{m} +} + +func NewVal(newTunnelEndpoint netip.Addr, vtepMAC mac.MAC) VtepPolicyVal { + mac, _ := vtepMAC.Uint64() + + value := VtepPolicyVal{ + Mac: mac, + } + + ip4 := newTunnelEndpoint.As4() + copy(value.VtepIp[:], ip4[:]) + + return value +} + +// OpenPinnedVtepPolicyMap opens an existing pinned IPv4 policy map. +func OpenPinnedVtepPolicyMap(logger *slog.Logger) (*VtepPolicyMap, error) { + m, err := bpf.OpenMap(bpf.MapPath(logger, VtepPolicyMapName), &VtepPolicyKey{}, &VtepPolicyVal{}) + if err != nil { + return nil, err + } + + return &VtepPolicyMap{m}, nil +} + +// Function to update vtep map with VTEP CIDR +func (m *VtepPolicyMap) UpdateVtepPolicyMapping(srcIP netip.Addr, dstCIDR netip.Prefix, newTunnelEndpoint netip.Addr, vtepMAC mac.MAC) error { + key := NewKey(srcIP, dstCIDR) + value := NewVal(newTunnelEndpoint, vtepMAC) + + return m.m.Update(&key, &value) +} + +func (m *VtepPolicyMap) RemoveVtepPolicyMapping(srcIP netip.Addr, dstCIDR netip.Prefix) error { + key := NewKey(srcIP, dstCIDR) + return m.m.Delete(&key) +} + +func (m *VtepPolicyMap) Delete(key *VtepPolicyKey) error { + return m.m.Delete(key) +} + +func (m *VtepPolicyMap) Lookup(key *VtepPolicyKey) (*VtepPolicyVal, error) { + ret, err := m.m.Lookup(key) + if err != nil { + return nil, err + } + return ret.(*VtepPolicyVal), err +} + +// VtepPolicyIterateCallback represents the signature of the callback function +// expected by the IterateWithCallback method, which in turn is used to iterate +// all the keys/values of an vtep policy map. +type VtepPolicyIterateCallback func(*VtepPolicyKey, *VtepPolicyVal) + +// IterateWithCallback iterates through all the keys/values of an vtep policy +// map, passing each key/value pair to the cb callback. +func (m *VtepPolicyMap) IterateWithCallback(cb VtepPolicyIterateCallback) error { + return m.m.DumpWithCallback(func(k bpf.MapKey, v bpf.MapValue) { + key := k.(*VtepPolicyKey) + value := v.(*VtepPolicyVal) + + cb(key, value) + }) +} + +func (k *VtepPolicyKey) Match(ip netip.Addr, destCIDR netip.Prefix) bool { + nkey := NewKey(ip, destCIDR) + return nkey == *k +} + +func (v *VtepPolicyVal) Match(vtepIP netip.Addr, rmac mac.MAC) bool { + nval := NewVal(vtepIP, rmac) + return nval == *v +} diff --git a/pkg/raw/raw.go b/pkg/raw/raw.go new file mode 100644 index 0000000000000..13cba9ff1fe02 --- /dev/null +++ b/pkg/raw/raw.go @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package raw + +import ( + "context" + "fmt" + "log/slog" + "net/netip" + + "github.com/cilium/cilium/pkg/annotation" + "github.com/cilium/cilium/pkg/endpointmanager" + "github.com/cilium/cilium/pkg/identity" + identityCache "github.com/cilium/cilium/pkg/identity/cache" + "github.com/cilium/cilium/pkg/k8s" + "github.com/cilium/cilium/pkg/k8s/resource" + slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" + k8sLabels "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels" + k8sTypes "github.com/cilium/cilium/pkg/k8s/types" + "github.com/cilium/cilium/pkg/labels" + "github.com/cilium/cilium/pkg/logging/logfields" + "github.com/cilium/cilium/pkg/maps/crap" + "github.com/cilium/cilium/pkg/node" + "github.com/cilium/hive/cell" + "github.com/cilium/hive/job" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" +) + +// Cell provides a [Manager] for consumption with hive. +var Cell = cell.Module( + "crap", + "Cilium Raw Acquisition of Packets", + // cell.Provide(newCrapManager), + cell.Provide(k8s.ServiceResource), + cell.Invoke(registerCrapManager), +) + +type CrapParams struct { + cell.In + + JobGroup job.Group + Services resource.Resource[*slim_corev1.Service] + Endpoints resource.Resource[*k8sTypes.CiliumEndpoint] + Logger *slog.Logger + IdentityAllocator identityCache.IdentityAllocator + EndpointManager endpointmanager.EndpointManager + BpfMap *crap.CrapMap +} + +type endpointMetadata struct { + labels map[string]string + id endpointID + ip netip.Addr + nodeIP string + ifindex int +} + +type serviceMetadata struct { + labels map[string]string + id serviceID + vip []netip.Addr +} + +type endpointID = types.UID +type serviceID = types.UID + +type diff struct { + serviceDiff map[serviceID]*serviceMetadata + serviceSync bool + + epDiff map[endpointID]*endpointMetadata + epSync bool +} + +func newDiff() *diff { + return &diff{ + epDiff: make(map[endpointID]*endpointMetadata), + epSync: false, + serviceDiff: make(map[serviceID]*serviceMetadata), + serviceSync: false, + } +} + +type CrapManager struct { + trigger job.Trigger + logger *slog.Logger + ch chan *diff + identityAllocator identityCache.IdentityAllocator + bpfmap *crap.CrapMap + endpointManager endpointmanager.EndpointManager +} + +func newCrapManager(params CrapParams) *CrapManager { + return &CrapManager{ + logger: params.Logger, + identityAllocator: params.IdentityAllocator, + ch: make(chan *diff, 64), + bpfmap: params.BpfMap, + endpointManager: params.EndpointManager, + } +} + +func (cm *CrapManager) getEndpointMetadata(endpoint *k8sTypes.CiliumEndpoint, identityLabels labels.Labels) (*endpointMetadata, error) { + var addr netip.Addr + var ifindex int + + if endpoint.UID == "" { + // this can happen when CiliumEndpointSlices are in use - which is not supported in the EGW yet + return nil, fmt.Errorf("endpoint has empty UID") + } + + if endpoint.Networking == nil { + return nil, fmt.Errorf("endpoint has no networking metadata") + } + + if len(endpoint.Networking.Addressing) == 0 { + return nil, fmt.Errorf("failed to get valid endpoint IPs") + } + + nodeIP := node.GetCiliumEndpointNodeIP(cm.logger) + + for _, pair := range endpoint.Networking.Addressing { + if pair.IPV4 != "" { + a, err := netip.ParseAddr(pair.IPV4) + + if err != nil { + continue + } + + if endpoint.Networking.NodeIP == nodeIP { + if ep := cm.endpointManager.LookupIP(addr); ep != nil { + ifindex = ep.GetIfIndex() + } + } + + addr = a + break + } + } + + data := &endpointMetadata{ + ip: addr, + labels: identityLabels.K8sStringMap(), + id: endpoint.UID, + nodeIP: endpoint.Networking.NodeIP, + ifindex: ifindex, + } + + return data, nil +} + +func getServiceMetadata(svc *slim_corev1.Service) (*serviceMetadata, error) { + var addrs []netip.Addr + + for _, pair := range svc.Spec.ExternalIPs { + addr, err := netip.ParseAddr(pair) + if err != nil { + continue + } + addrs = append(addrs, addr) + } + + data := &serviceMetadata{ + vip: addrs, + labels: svc.Spec.Selector, + id: svc.UID, + } + + return data, nil +} + +func (cm *CrapManager) addService(ctx context.Context, svc *slim_corev1.Service, diff *diff) error { + var svcData *serviceMetadata + var err error + + log := cm.logger.With( + logfields.ID, svc.UID, + logfields.K8sNamespace, svc.Namespace, + logfields.Name, svc.Name, + logfields.Labels, svc.Labels, + ) + + if _, ok := annotation.Get(svc, annotation.ServiceRaw); !ok { + log.DebugContext(ctx, "Skip services w/o RAW annotation") + return nil + } + + log.InfoContext(ctx, "Adding new service") + + if svcData, err = getServiceMetadata(svc); err != nil { + log.WarnContext(ctx, "Failed to get valid service metadata", logfields.Error, err) + return nil + } + + log.DebugContext(ctx, "Service accepted for adding/updating") + diff.serviceDiff[svcData.id] = svcData + + return nil +} + +func (cm *CrapManager) delService(ctx context.Context, svc *slim_corev1.Service, diff *diff) error { + log := cm.logger.With( + logfields.ID, svc.UID, + logfields.K8sNamespace, svc.Namespace, + logfields.Name, svc.Name, + logfields.Labels, svc.Labels, + ) + + log.InfoContext(ctx, "Removing service") + diff.serviceDiff[svc.UID] = nil + + return nil +} + +func (cm *CrapManager) handleSvcEvent(ctx context.Context, event resource.Event[*slim_corev1.Service]) error { + var err error = nil + + diff := newDiff() + svc := event.Object + + switch event.Kind { + case resource.Sync: + diff.serviceSync = true + case resource.Upsert: + cm.addService(ctx, svc, diff) + case resource.Delete: + cm.delService(ctx, svc, diff) + } + + event.Done(err) + cm.ch <- diff + + return err +} + +func (manager *CrapManager) getIdentityLabels(securityIdentity uint32) (labels.Labels, error) { + if err := manager.identityAllocator.WaitForInitialGlobalIdentities(context.Background()); err != nil { + return nil, fmt.Errorf("failed to wait for initial global identities: %w", err) + } + + identity := manager.identityAllocator.LookupIdentityByID(context.Background(), identity.NumericIdentity(securityIdentity)) + if identity == nil { + return nil, fmt.Errorf("identity %d not found", securityIdentity) + } + return identity.Labels, nil +} + +func (cm *CrapManager) addEndpoint(ctx context.Context, ep *k8sTypes.CiliumEndpoint, diff *diff) error { + var epData *endpointMetadata + var err error + var identityLabels labels.Labels + + log := cm.logger.With( + logfields.K8sNamespace, ep.Namespace, + logfields.Name, ep.Name, + logfields.Labels, ep.Labels, + ) + + log.DebugContext(ctx, "Adding new endpoint") + + if ep.Identity == nil { + log.Warn("Endpoint is missing identity metadata, skipping update to raw rules") + return nil + } + + if identityLabels, err = cm.getIdentityLabels(uint32(ep.Identity.ID)); err != nil { + log.WarnContext(ctx, "Failed to get identity labels for endpoint", logfields.Error, err) + return err + } + + if epData, err = cm.getEndpointMetadata(ep, identityLabels); err != nil { + log.ErrorContext(ctx, "Failed to get valid endpoint metadata, skipping update to raw rules", logfields.Error, err) + return nil + } + + log.DebugContext(ctx, "CiliumEndpoint accepted for adding/updating") + diff.epDiff[epData.id] = epData + + return nil +} + +func (cm *CrapManager) delEndpoint(ctx context.Context, ep *k8sTypes.CiliumEndpoint, diff *diff) error { + log := cm.logger.With( + logfields.K8sNamespace, ep.Namespace, + logfields.Name, ep.Name, + logfields.Labels, ep.Labels, + ) + + log.InfoContext(ctx, "Removing endpoint") + diff.epDiff[ep.UID] = nil + + return nil +} + +func (cm *CrapManager) handleEndpointEvent(ctx context.Context, event resource.Event[*k8sTypes.CiliumEndpoint]) error { + var err error = nil + + diff := newDiff() + ep := event.Object + + switch event.Kind { + case resource.Sync: + diff.epSync = true + case resource.Upsert: + cm.addEndpoint(ctx, ep, diff) + case resource.Delete: + cm.delEndpoint(ctx, ep, diff) + } + + event.Done(err) + cm.ch <- diff + + return err +} + +func (config *serviceMetadata) matchesPodLabels(epLabels map[string]string) bool { + if len(config.labels) == 0 { + return false + } + + selector := k8sLabels.SelectorFromSet(config.labels) + toMatch := k8sLabels.Set(epLabels) + return selector.Matches(toMatch) +} + +func buildRules(eps map[endpointID]*endpointMetadata, svcs map[serviceID]*serviceMetadata) map[crap.CrapKey]crap.CrapVal { + ret := make(map[crap.CrapKey]crap.CrapVal) + + for _, svc := range svcs { + var targetEp *endpointMetadata = nil + + for _, ep := range eps { + if svc.matchesPodLabels(ep.labels) { + targetEp = ep + break + } + } + + if targetEp == nil { + continue + } + + for _, ip := range svc.vip { + key := crap.NewKey(ip) + val := crap.NewVal(targetEp.ip) + + ret[key] = val + } + } + + return ret +} + +func (cm *CrapManager) updateRawRules(desired map[crap.CrapKey]crap.CrapVal) { + if cm.bpfmap == nil { + cm.logger.Error("bpf map is nil") + return + } + + existing := make(map[crap.CrapKey]crap.CrapVal) + cm.bpfmap.IterateWithCallback( + func(key *crap.CrapKey, val *crap.CrapVal) { + existing[*key] = *val + }) + + // Start with the assumption that all the entries currently present in the + // BPF map are stale. Then as we walk the entries below and discover which + // entries are actually still needed, shrink this set down. + stale := sets.KeySet(existing) + + for key, val := range desired { + logger := cm.logger.With( + logfields.DestinationIP, key.DestIP, + ) + + stale.Delete(key) + + if err := cm.bpfmap.Update(key, val); err != nil { + logger.Error("Error applying raw rule", logfields.Error, err) + } else { + logger.Debug("rule was updated") + } + + } + + // Remove all the entries marked as stale. + for key := range stale { + logger := cm.logger.With( + logfields.DestinationIP, key.DestIP, + ) + + if err := cm.bpfmap.Delete(&key); err != nil { + logger.Error("Error removing raw rule", logfields.Error, err) + } else { + logger.Debug("Raw rule was removed") + } + } +} + +func (cm *CrapManager) reconcile(ctx context.Context, health cell.Health) error { + epDataStore := make(map[endpointID]*endpointMetadata) + svcDataStore := make(map[serviceID]*serviceMetadata) + sync, syncEps, syncSvcs := false, false, false + + for { + select { + case d := <-cm.ch: + for id, endpoint := range d.epDiff { + if endpoint == nil { + cm.logger.InfoContext(ctx, "removed an endpoint", logfields.ID, id) + delete(epDataStore, id) + } else { + cm.logger.InfoContext(ctx, "stored an endpoint", logfields.ID, id) + epDataStore[endpoint.id] = endpoint + } + } + + for id, svc := range d.serviceDiff { + if svc == nil { + cm.logger.InfoContext(ctx, "removed a service", logfields.ID, id) + delete(svcDataStore, id) + } else { + cm.logger.InfoContext(ctx, "stored a service", logfields.ID, id) + svcDataStore[svc.id] = svc + } + } + + if !sync { + if d.epSync && !syncEps { + cm.logger.InfoContext(ctx, "Endpoints were synced") + syncEps = d.epSync + } + + if d.serviceSync && !syncSvcs { + cm.logger.InfoContext(ctx, "Services were synced") + syncSvcs = d.serviceSync + } + + sync = syncEps && syncSvcs + } + + if !sync { + continue + } + + desired := buildRules(epDataStore, svcDataStore) + cm.updateRawRules(desired) + + case <-ctx.Done(): + return nil + } + } +} + +func registerCrapManager(params CrapParams) { + mgr := newCrapManager(params) + mgr.trigger = job.NewTrigger() + + // --- observers: signal only --- + params.JobGroup.Add(job.Observer( + "raw-service-observer", + mgr.handleSvcEvent, + params.Services, + )) + + params.JobGroup.Add(job.Observer( + "raw-endpoint-observer", + mgr.handleEndpointEvent, + params.Endpoints, + )) + + params.JobGroup.Add(job.OneShot( + "raw-reconciler", + mgr.reconcile, + )) +} diff --git a/pkg/vteppolicy/doc.go b/pkg/vteppolicy/doc.go new file mode 100644 index 0000000000000..c531ebefa32c6 --- /dev/null +++ b/pkg/vteppolicy/doc.go @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +// Package vteppolicy defines an internal representation of the Cilium VTEP +// Policy. The structures are managed by the Manager. +package vteppolicy diff --git a/pkg/vteppolicy/endpoint.go b/pkg/vteppolicy/endpoint.go new file mode 100644 index 0000000000000..61e0fa8281db8 --- /dev/null +++ b/pkg/vteppolicy/endpoint.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "fmt" + "net/netip" + + "k8s.io/apimachinery/pkg/types" + + k8sTypes "github.com/cilium/cilium/pkg/k8s/types" + "github.com/cilium/cilium/pkg/labels" +) + +// endpointMetadata stores relevant metadata associated with a endpoint that's updated during endpoint +// add/update events +type endpointMetadata struct { + // Endpoint labels + labels map[string]string + // Endpoint ID + id endpointID + // ips are endpoint's unique IPs + ips []netip.Addr + // nodeIP is the IP of the node the endpoint is on + nodeIP string +} + +// endpointID is based on endpoint's UID +type endpointID = types.UID + +func getEndpointMetadata(endpoint *k8sTypes.CiliumEndpoint, identityLabels labels.Labels) (*endpointMetadata, error) { + var addrs []netip.Addr + + if endpoint.UID == "" { + // this can happen when CiliumEndpointSlices are in use - which is not supported in the EGW yet + return nil, fmt.Errorf("endpoint has empty UID") + } + + if endpoint.Networking == nil { + return nil, fmt.Errorf("endpoint has no networking metadata") + } + + if len(endpoint.Networking.Addressing) == 0 { + return nil, fmt.Errorf("failed to get valid endpoint IPs") + } + + for _, pair := range endpoint.Networking.Addressing { + if pair.IPV4 != "" { + addr, err := netip.ParseAddr(pair.IPV4) + if err != nil { + continue + } + addrs = append(addrs, addr) + } + } + + data := &endpointMetadata{ + ips: addrs, + labels: identityLabels.K8sStringMap(), + id: endpoint.UID, + nodeIP: endpoint.Networking.NodeIP, + } + + return data, nil +} diff --git a/pkg/vteppolicy/helpers_test.go b/pkg/vteppolicy/helpers_test.go new file mode 100644 index 0000000000000..607c27f0f0ff3 --- /dev/null +++ b/pkg/vteppolicy/helpers_test.go @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "context" + "errors" + "net/netip" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + "github.com/cilium/cilium/pkg/k8s/resource" + slimv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" + k8sTypes "github.com/cilium/cilium/pkg/k8s/types" + "github.com/cilium/cilium/pkg/mac" + "github.com/cilium/cilium/pkg/policy/api" + policyTypes "github.com/cilium/cilium/pkg/policy/types" +) + +type fakeResource[T runtime.Object] chan resource.Event[T] + +func (fr fakeResource[T]) sync(tb testing.TB) { + var sync resource.Event[T] + sync.Kind = resource.Sync + fr.process(tb, sync) +} + +func (fr fakeResource[T]) process(tb testing.TB, ev resource.Event[T]) { + tb.Helper() + if err := fr.processWithError(ev); err != nil { + tb.Fatal("Failed to process event:", err) + } +} + +func (fr fakeResource[T]) processWithError(ev resource.Event[T]) error { + errs := make(chan error) + ev.Done = func(err error) { + errs <- err + } + fr <- ev + return <-errs +} + +func (fr fakeResource[T]) Observe(ctx context.Context, next func(event resource.Event[T]), complete func(error)) { + complete(errors.New("not implemented")) +} + +func (fr fakeResource[T]) Events(ctx context.Context, opts ...resource.EventsOpt) <-chan resource.Event[T] { + if len(opts) > 1 { + // Ideally we'd only ignore resource.WithRateLimit here, but that + // isn't possible. + panic("more than one option is not supported") + } + return fr +} + +func (fr fakeResource[T]) Store(context.Context) (resource.Store[T], error) { + return nil, errors.New("not implemented") +} + +func addPolicy(tb testing.TB, policies fakeResource[*Policy], params *policyParams) { + tb.Helper() + + policy, _ := newCVP(params) + policies.process(tb, resource.Event[*Policy]{ + Kind: resource.Upsert, + Object: policy, + }) +} + +type policyParams struct { + name string + endpointLabels map[string]string + podSelectors map[string]string + destinationCIDRs []string + podLabels map[string]string + vtepIP string + mac string +} + +func newCVP(params *policyParams) (*v2alpha1.CiliumVtepPolicy, *PolicyConfig) { + parsedDestinationCIDRs := make([]netip.Prefix, 0, len(params.destinationCIDRs)) + for _, destCIDR := range params.destinationCIDRs { + parsedDestinationCIDR, _ := netip.ParsePrefix(destCIDR) + parsedDestinationCIDRs = append(parsedDestinationCIDRs, parsedDestinationCIDR) + } + + parsedVtepIp, _ := netip.ParseAddr(params.vtepIP) + parsedMac, _ := mac.ParseMAC(params.mac) + + policy := &PolicyConfig{ + id: types.NamespacedName{ + Name: params.name, + }, + dstCIDRs: parsedDestinationCIDRs, + vtepConfig: vtepConfig{ + vtepIP: parsedVtepIp, + vtepMAC: parsedMac, + }, + } + + if len(params.podSelectors) != 0 { + policy.podSelectors = []*policyTypes.LabelSelector{ + policyTypes.NewLabelSelector(api.EndpointSelector{ + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: params.endpointLabels, + }, + }), + } + } + + // Create destination CIDRs list + var destinationCIDRs []v2alpha1.CIDR + for _, destCIDR := range params.destinationCIDRs { + destinationCIDRs = append(destinationCIDRs, v2alpha1.CIDR(destCIDR)) + } + + cvp := &v2alpha1.CiliumVtepPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: params.name, + }, + Spec: v2alpha1.CiliumVtepPolicySpec{ + Selectors: []v2alpha1.CiliumVtepPolicyRules{ + { + PodSelector: &slimv1.LabelSelector{ + MatchLabels: params.endpointLabels, + }, + }, + }, + DestinationCIDRs: destinationCIDRs, + ExternalVTEP: &v2alpha1.ExternalVTEP{ + IP: params.vtepIP, + MAC: v2alpha1.MAC(params.mac), + }, + }, + TypeMeta: metav1.TypeMeta{}, + } + + if len(params.podSelectors) != 0 { + cvp.Spec.Selectors[0].PodSelector = &slimv1.LabelSelector{ + MatchLabels: params.podSelectors, + } + } + + return cvp, policy +} + +func addEndpoint(tb testing.TB, endpoints fakeResource[*k8sTypes.CiliumEndpoint], ep *k8sTypes.CiliumEndpoint) { + endpoints.process(tb, resource.Event[*k8sTypes.CiliumEndpoint]{ + Kind: resource.Upsert, + Object: ep, + }) +} + +func deleteEndpoint(tb testing.TB, endpoints fakeResource[*k8sTypes.CiliumEndpoint], ep *k8sTypes.CiliumEndpoint) { + endpoints.process(tb, resource.Event[*k8sTypes.CiliumEndpoint]{ + Kind: resource.Delete, + Object: ep, + }) +} diff --git a/pkg/vteppolicy/manager.go b/pkg/vteppolicy/manager.go new file mode 100644 index 0000000000000..6f62ab0601825 --- /dev/null +++ b/pkg/vteppolicy/manager.go @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "context" + "fmt" + "log/slog" + "net/netip" + "sync" + "sync/atomic" + + "github.com/cilium/hive/cell" + "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/util/workqueue" + + "github.com/cilium/cilium/pkg/datapath/linux/config/defines" + "github.com/cilium/cilium/pkg/datapath/tunnel" + "github.com/cilium/cilium/pkg/identity" + identityCache "github.com/cilium/cilium/pkg/identity/cache" + "github.com/cilium/cilium/pkg/k8s/resource" + k8sTypes "github.com/cilium/cilium/pkg/k8s/types" + "github.com/cilium/cilium/pkg/labels" + "github.com/cilium/cilium/pkg/logging/logfields" + "github.com/cilium/cilium/pkg/maps/vtep_policy" + "github.com/cilium/cilium/pkg/option" + "github.com/cilium/cilium/pkg/time" +) + +// Cell provides a [Manager] for consumption with hive. +var Cell = cell.Module( + "vteppolicy", + "Vtep Policy allows to use external VTEPs access pods", + cell.Config(defaultConfig), + cell.Provide(NewVtepPolicyManager), + cell.Provide(newPolicyResource), + cell.Provide(func(dcfg *option.DaemonConfig) tunnel.EnablerOut { + if !dcfg.EnableVTEP { + return tunnel.EnablerOut{} + } + return tunnel.NewEnabler(true) + }), +) + +type Config struct { + // Default amount of time between triggers of vtep policy state + // reconciliations are invoked + VtepPolicyReconciliationTriggerInterval time.Duration +} + +var defaultConfig = Config{ + VtepPolicyReconciliationTriggerInterval: 1 * time.Second, +} + +func (def Config) Flags(flags *pflag.FlagSet) { + flags.Duration("vtep-policy-reconciliation-trigger-interval", def.VtepPolicyReconciliationTriggerInterval, "Time between triggers of vtep policy state reconciliations") +} + +// The vtep policy manager stores the internal data tracking the node, policy, +// endpoint, and lease mappings. It also hooks up all the callbacks to update +// vteppolicy bpf policy map accordingly. +type Manager struct { + logger *slog.Logger + // reconciliationEventsCount keeps track of how many reconciliation events have occurred. + reconciliationEventsCount atomic.Uint64 + + // policies allow reading policy CRD from k8s. + policies resource.Resource[*Policy] + + // endpoints allow reading endpoint CRD from k8s. + endpoints resource.Resource[*k8sTypes.CiliumEndpoint] + + // identityAllocator is used to fetch identity labels for endpoint updates + identityAllocator identityCache.IdentityAllocator + + // policyMap4 communicates the active IPv4 policies to the datapath. + policyMap *vtep_policy.VtepPolicyMap +} + +// vtepDiffs stores deduplicated events diff from events. +type vtepDiffs struct { + // endpointsDiff stores deduplicated endpoints diff from events. + endpointsDiff map[endpointID]*endpointMetadata + // policiesDiff stores deduplicated policies diff from events. + policiesDiff map[policyID]*PolicyConfig + // policySync is set to true when policy sync event arrived. + policySync bool + // endpointSync is set to true when endpoint sync event arrived. + endpointSync bool +} + +func newVtepDiffs() *vtepDiffs { + return &vtepDiffs{ + endpointsDiff: make(map[endpointID]*endpointMetadata), + policiesDiff: make(map[policyID]*PolicyConfig), + } +} + +// reconcile waits for the policy and endpoint changes, and then runs the reconciliation for them. +// It reconciles the desired state by updating the internal cache for policies and endpoints. +// It tries to reconcile the desired state by updating the vteppolicy bpf map entries. +func (manager *Manager) reconcile(ctx context.Context, ch <-chan *vtepDiffs) { + // epDataStore stores desired endpointId to endpoint metadata mapping. + epDataStore := make(map[endpointID]*endpointMetadata) + // policyConfigs stores desired policy configs indexed by policyID. + policyConfigs := make(map[policyID]*PolicyConfig) + reasons := make(map[string]uint32) + var sync, policySync, endpointSync bool + for { + select { + case <-ctx.Done(): + return + case d := <-ch: + // Apply endpoints' diff. + for id, endpoint := range d.endpointsDiff { + if endpoint == nil { + delete(epDataStore, id) + reasons["policy deleted"]++ + } else { + if _, ok := epDataStore[endpoint.id]; ok { + reasons["endpoint updated"]++ + } else { + reasons["endpoint added"]++ + } + epDataStore[endpoint.id] = endpoint + } + } + + // Apply policies' diff. + for id, policy := range d.policiesDiff { + if policy == nil { + if _, ok := policyConfigs[id]; ok { + delete(policyConfigs, id) + reasons["policy deleted"]++ + } else { + manager.logger.Warn("Can't delete CiliumVtepPolicy: policy not found") + } + } else { + if _, ok := policyConfigs[policy.id]; ok { + reasons["policy updated"]++ + } else { + reasons["policy added"]++ + } + policyConfigs[policy.id] = policy + } + } + + if !sync { + if d.policySync { + policySync = true + } + if d.endpointSync { + endpointSync = true + } + + if sync = policySync && endpointSync; !sync { + manager.logger.Debug("reconciliation skips", logfields.Reason, reasons, + "policies sync", policySync, "endpoints sync", endpointSync) + continue + } + } + + manager.logger.Debug("reconciliation starts", logfields.Reason, reasons) + reasons = make(map[string]uint32) + + for _, policy := range policyConfigs { + policy.updateMatchedEndpointIDs(epDataStore) + } + + manager.updateVtepRules(policyConfigs) + + manager.reconciliationEventsCount.Add(1) + } + } +} + +type Params struct { + cell.In + + Logger *slog.Logger + + Config Config + DaemonConfig *option.DaemonConfig + IdentityAllocator identityCache.IdentityAllocator + PolicyMap *vtep_policy.VtepPolicyMap + Policies resource.Resource[*Policy] + Endpoints resource.Resource[*k8sTypes.CiliumEndpoint] + + Lifecycle cell.Lifecycle +} + +func NewVtepPolicyManager(p Params) (out struct { + cell.Out + + *Manager + defines.NodeOut +}, err error) { + dcfg := p.DaemonConfig + out.Manager = nil + + if !dcfg.EnableVTEP { + return out, fmt.Errorf("vtep policy requires --%s=\"true\" ", option.EnableVTEP) + } + + out.Manager, err = newVtepPolicyManager(p) + if err != nil { + return out, err + } + + return out, nil +} + +func newVtepPolicyManager(p Params) (*Manager, error) { + manager := &Manager{ + logger: p.Logger.With(slog.String("manager", "vteppolicy")), + identityAllocator: p.IdentityAllocator, + policies: p.Policies, + policyMap: p.PolicyMap, + endpoints: p.Endpoints, + } + + var wg sync.WaitGroup + + ctx, cancel := context.WithCancel(context.Background()) + p.Lifecycle.Append(cell.Hook{ + OnStart: func(hc cell.HookContext) error { + ch := make(chan *vtepDiffs) + + wg.Go(func() { + manager.processEvents(ctx, ch, p.Config.VtepPolicyReconciliationTriggerInterval) + }) + + wg.Go(func() { + manager.reconcile(ctx, ch) + }) + + return nil + }, + OnStop: func(hc cell.HookContext) error { + cancel() + + wg.Wait() + return nil + }, + }) + + return manager, nil +} + +// getIdentityLabels waits for the global identities to be populated to the cache, +// then looks up identity by ID from the cached identity allocator and return its labels. +func (manager *Manager) getIdentityLabels(securityIdentity uint32) (labels.Labels, error) { + if err := manager.identityAllocator.WaitForInitialGlobalIdentities(context.Background()); err != nil { + return nil, fmt.Errorf("failed to wait for initial global identities: %w", err) + } + + identity := manager.identityAllocator.LookupIdentityByID(context.Background(), identity.NumericIdentity(securityIdentity)) + if identity == nil { + return nil, fmt.Errorf("identity %d not found", securityIdentity) + } + return identity.Labels, nil +} + +// processEvents collects policy and endpoint events from K8S and sends them to the reconciler periodically. +// It also waits for the required duration between events to avoid excessive reconciliation. +func (manager *Manager) processEvents(ctx context.Context, ch chan<- *vtepDiffs, minInterval time.Duration) { + // here we try to mimic the same exponential backoff retry logic used by + // the identity allocator, where the minimum retry timeout is set to 20 + // milliseconds and the max number of attempts is 16 (so 20ms * 2^16 == + // ~20 minutes) + endpointsRateLimit := workqueue.NewTypedItemExponentialFailureRateLimiter[resource.WorkItem]( + time.Millisecond*20, + time.Minute*20, + ) + + policyEvents := manager.policies.Events(ctx) + endpointEvents := manager.endpoints.Events(ctx, resource.WithRateLimiter(endpointsRateLimit)) + + diffs := newVtepDiffs() + minDur := NewMinDuration(minInterval) + var r *retry + + for { + select { + case <-ctx.Done(): + r.Stop() + return + case <-r.GetChannel(): + case <-minDur.GetChannel(): + // For nil channel it will never be fired. + case event := <-policyEvents: + manager.handlePolicyEvent(event, diffs) + case event := <-endpointEvents: + manager.handleEndpointEvent(event, diffs) + } + + r.Stop() + r = nil + + if !minDur.Check() { + // Go to the above loop and wait until the required duration has passed. + // When it waits for the required duration, it can be woken up by the policy or endpoint channel. + // Thanks to this approach, it is possible to collect more events (without blocking channels), or + // react on cancellation of the context. + continue + } + + select { + case ch <- diffs: + // It was sent successfully to the applier, so now collect next events. + diffs = newVtepDiffs() + minDur.SetLastCheck() + default: + // Reconciliation is in progress, so collect more events here and try to send them later. + // Try again in 1 second. + r = newRetry(time.Second) + } + } +} + +func (manager *Manager) handlePolicyEvent(event resource.Event[*Policy], diffs *vtepDiffs) { + var err error + + switch event.Kind { + case resource.Sync: + diffs.policySync = true + case resource.Upsert: + err = manager.onAddVtepPolicy(event.Object, diffs) + case resource.Delete: + manager.onDeleteVtepPolicy(event.Object, diffs) + } + + event.Done(err) +} + +// Event handlers + +// onAddVtepPolicy parses the given policy config and populates it to a policy channel. +func (manager *Manager) onAddVtepPolicy(policy *Policy, diffs *vtepDiffs) error { + logger := manager.logger.With(logfields.CiliumVtepPolicyName, policy.Name) + + config, err := ParseCVP(policy) + if err != nil { + logger.Warn("Failed to parse CiliumVtepPolicy", logfields.Error, err) + return err + } + + logger.Debug("CiliumVtepPolicy accepted for adding/updating") + diffs.policiesDiff[config.id] = config + + return nil +} + +// onDeleteVtepPolicy populates event to a policy channel. +func (manager *Manager) onDeleteVtepPolicy(policy *Policy, diffs *vtepDiffs) { + configID := ParseCVPConfigID(policy) + + logger := manager.logger.With(logfields.CiliumVtepPolicyName, configID.Name) + logger.Debug("CiliumVtepPolicy accepted for deletion") + + diffs.policiesDiff[configID] = nil +} + +func (manager *Manager) addEndpoint(endpoint *k8sTypes.CiliumEndpoint, diffs *vtepDiffs) error { + var epData *endpointMetadata + var err error + var identityLabels labels.Labels + + logger := manager.logger.With( + logfields.K8sEndpointName, endpoint.Name, + logfields.K8sNamespace, endpoint.Namespace, + logfields.K8sUID, endpoint.UID, + ) + + if endpoint.Identity == nil { + logger.Warn("Endpoint is missing identity metadata, skipping update to vtep policy.") + return nil + } + + if identityLabels, err = manager.getIdentityLabels(uint32(endpoint.Identity.ID)); err != nil { + logger.Warn("Failed to get identity labels for endpoint", logfields.Error, err) + return err + } + + if epData, err = getEndpointMetadata(endpoint, identityLabels); err != nil { + logger.Error("Failed to get valid endpoint metadata, skipping update to vtep policy.", logfields.Error, err) + return nil + } + + logger.Debug("CiliumEndpoint accepted for adding/updating") + diffs.endpointsDiff[epData.id] = epData + + return nil +} + +func (manager *Manager) deleteEndpoint(endpoint *k8sTypes.CiliumEndpoint, diffs *vtepDiffs) { + logger := manager.logger.With( + logfields.K8sEndpointName, endpoint.Name, + logfields.K8sNamespace, endpoint.Namespace, + logfields.K8sUID, endpoint.UID, + ) + + logger.Debug("CiliumEndpoint accepted for deletion") + diffs.endpointsDiff[endpoint.UID] = nil +} + +func (manager *Manager) handleEndpointEvent(event resource.Event[*k8sTypes.CiliumEndpoint], diffs *vtepDiffs) { + endpoint := event.Object + var err error + + switch event.Kind { + case resource.Sync: + diffs.endpointSync = true + case resource.Upsert: + err = manager.addEndpoint(endpoint, diffs) + default: + manager.deleteEndpoint(endpoint, diffs) + } + + event.Done(err) +} + +// updateVtepRules updates the content of the BPF maps. +// Whenever an error occurs, it will just log it and move to the next item, +// in order to reconcile as many states as possible. +func (manager *Manager) updateVtepRules(policyConfigs map[policyID]*PolicyConfig) { + if manager.policyMap == nil { + manager.logger.Error("policyMap is nil") + return + } + + vtepPolicies := map[vtep_policy.VtepPolicyKey]vtep_policy.VtepPolicyVal{} + manager.policyMap.IterateWithCallback( + func(key *vtep_policy.VtepPolicyKey, val *vtep_policy.VtepPolicyVal) { + vtepPolicies[*key] = *val + }) + + // Start with the assumption that all the entries currently present in the + // BPF map are stale. Then as we walk the entries below and discover which + // entries are actually still needed, shrink this set down. + stale := sets.KeySet(vtepPolicies) + + addVtepRule := func(endpointIP netip.Addr, dstCIDR netip.Prefix, vtep *vtepConfig) { + if !endpointIP.Is4() { + return + } + + if !dstCIDR.Addr().Is4() { + return + } + + if vtep == nil { + return + } + + policyKey := vtep_policy.NewKey(endpointIP, dstCIDR) + // This key needs to be present in the BPF map, hence remove it from + // the list of stale ones. + stale.Delete(policyKey) + + logger := manager.logger.With( + logfields.SourceIP, endpointIP, + logfields.DestinationCIDR, dstCIDR.String(), + logfields.VtepIP, vtep.vtepIP, + logfields.VtepMAC, vtep.vtepMAC, + ) + + if err := manager.policyMap.UpdateVtepPolicyMapping(endpointIP, dstCIDR, vtep.vtepIP, vtep.vtepMAC); err != nil { + logger.Error("Error applying vtep policy", logfields.Error, err) + } else { + logger.Debug("vtep policy applied") + } + } + + for _, policyConfig := range policyConfigs { + policyConfig.forEachEndpointAndCIDR(addVtepRule) + } + + // Remove all the entries marked as stale. + for policyKey := range stale { + logger := manager.logger.With( + logfields.SourceIP, policyKey.SourceIP, + logfields.DestinationCIDR, policyKey.DestCIDR.String(), + ) + + if err := manager.policyMap.Delete(&policyKey); err != nil { + logger.Error("Error removing vtep gateway policy", logfields.Error, err) + } else { + logger.Debug("Vtep gateway policy removed") + } + } +} diff --git a/pkg/vteppolicy/manager_privileged_test.go b/pkg/vteppolicy/manager_privileged_test.go new file mode 100644 index 0000000000000..e9cdc09f92f99 --- /dev/null +++ b/pkg/vteppolicy/manager_privileged_test.go @@ -0,0 +1,526 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "context" + "fmt" + "net/netip" + "testing" + "time" + + "github.com/cilium/ebpf/rlimit" + "github.com/cilium/hive/hivetest" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/vishvananda/netlink" + "k8s.io/apimachinery/pkg/types" + + "github.com/cilium/cilium/pkg/bpf" + "github.com/cilium/cilium/pkg/datapath/linux/safenetlink" + "github.com/cilium/cilium/pkg/hive" + "github.com/cilium/cilium/pkg/identity" + cilium_api_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" + "github.com/cilium/cilium/pkg/k8s/resource" + slimv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" + k8sTypes "github.com/cilium/cilium/pkg/k8s/types" + "github.com/cilium/cilium/pkg/labels" + "github.com/cilium/cilium/pkg/mac" + "github.com/cilium/cilium/pkg/maps/vtep_policy" + "github.com/cilium/cilium/pkg/node/addressing" + nodeTypes "github.com/cilium/cilium/pkg/node/types" + "github.com/cilium/cilium/pkg/option" + "github.com/cilium/cilium/pkg/testutils" + testidentity "github.com/cilium/cilium/pkg/testutils/identity" +) + +const ( + testInterface1 = "cilium_egw1" + testInterface2 = "cilium_egw2" + + vtepIP1 = "1.2.3.4" + mac1 = "00:11:22:33:44:55" + + vtepIP2 = "1.2.3.5" + mac2 = "00:11:22:33:44:56" + + node1 = "k8s1" + node2 = "k8s2" + + node1IP = "192.168.1.1" + node2IP = "192.168.1.2" + + ep1IP = "10.0.0.1" + ep2IP = "10.0.0.2" + ep3IP = "10.0.0.3" + + destCIDR = "1.1.1.0/24" + + egressCIDR1 = "192.168.101.1/24" + egressCIDR2 = "192.168.102.1/24" +) + +var ( + ep1Labels = map[string]string{"test-key": "test-value-1"} + ep2Labels = map[string]string{"test-key": "test-value-2"} + + identityAllocator = testidentity.NewMockIdentityAllocator(nil) + + nodeGroup1Labels = map[string]string{"label1": "1"} + nodeGroup2Labels = map[string]string{"label2": "2"} +) + +type vtepRule struct { + sourceIP string + destCIDR string + vtepIP string + vtepMAC string +} + +type parsedVtepRule struct { + sourceIP netip.Addr + destCIDR netip.Prefix + vtepIP netip.Addr + vtepMAC mac.MAC +} + +type VtepPolicyTestSuite struct { + manager *Manager + policies fakeResource[*Policy] + nodes fakeResource[*cilium_api_v2.CiliumNode] + endpoints fakeResource[*k8sTypes.CiliumEndpoint] +} + +func setupVtepPolicyTestSuite(t *testing.T) *VtepPolicyTestSuite { + testutils.PrivilegedTest(t) + + logger := hivetest.Logger(t) + + bpf.CheckOrMountFS(logger, "") + + if _, err := vtep_policy.OpenPinnedVtepPolicyMap(logger); err != nil { + println(err) + } + + err := rlimit.RemoveMemlock() + require.NoError(t, err) + + nodeTypes.SetName(node1) + + k := &VtepPolicyTestSuite{} + k.policies = make(fakeResource[*Policy]) + k.nodes = make(fakeResource[*cilium_api_v2.CiliumNode]) + k.endpoints = make(fakeResource[*k8sTypes.CiliumEndpoint]) + + lc := hivetest.Lifecycle(t) + policyMap := vtep_policy.CreatePrivatePolicyMap(lc, nil) + + k.manager, err = newVtepPolicyManager(Params{ + Logger: logger, + Lifecycle: lc, + Config: Config{1 * time.Millisecond}, + DaemonConfig: &option.DaemonConfig{}, + IdentityAllocator: identityAllocator, + Policies: k.policies, + Endpoints: k.endpoints, + PolicyMap: policyMap, + }) + require.NoError(t, err) + require.NotNil(t, k.manager) + + return k +} + +func TestPrivilegedVtepPolicyCVPParser(t *testing.T) { + setupVtepPolicyTestSuite(t) + // must specify name + policy := policyParams{ + name: "", + destinationCIDRs: []string{destCIDR}, + } + + cvp, _ := newCVP(&policy) + _, err := ParseCVP(cvp) + require.Error(t, err) + + // catch nil DestinationCIDR field + policy = policyParams{ + name: "policy-1", + } + + cvp, _ = newCVP(&policy) + cvp.Spec.DestinationCIDRs = nil + _, err = ParseCVP(cvp) + require.Error(t, err) + + // must specify at least one DestinationCIDR + policy = policyParams{ + name: "policy-1", + } + + cvp, _ = newCVP(&policy) + _, err = ParseCVP(cvp) + require.Error(t, err) + + // catch nil VtepPolicy field + policy = policyParams{ + name: "policy-1", + destinationCIDRs: []string{destCIDR}, + } + + // must specify some sort of endpoint selector + policy = policyParams{ + name: "policy-1", + destinationCIDRs: []string{destCIDR}, + } + + cvp, _ = newCVP(&policy) + cvp.Spec.Selectors[0].NamespaceSelector = nil + cvp.Spec.Selectors[0].PodSelector = nil + _, err = ParseCVP(cvp) + require.Error(t, err) +} + +func TestPrivilegedVtepPolicyManager(t *testing.T) { + k := setupVtepPolicyTestSuite(t) + createTestInterface(t, testInterface1, []string{egressCIDR1}) + createTestInterface(t, testInterface2, []string{egressCIDR2}) + + vtepPolicyManager := k.manager + reconciliationEventsCount := vtepPolicyManager.reconciliationEventsCount.Load() + policyMap := k.manager.policyMap + + k.policies.sync(t) + k.nodes.sync(t) + k.endpoints.sync(t) + + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + node1 := newCiliumNode(node1, node1IP, nodeGroup1Labels) + k.nodes.process(t, resource.Event[*cilium_api_v2.CiliumNode]{ + Kind: resource.Upsert, + Object: node1.ToCiliumNode(), + }) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + node2 := newCiliumNode(node2, node2IP, nodeGroup2Labels) + k.nodes.process(t, resource.Event[*cilium_api_v2.CiliumNode]{ + Kind: resource.Upsert, + Object: node2.ToCiliumNode(), + }) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + // Create a new policy + policy1 := policyParams{ + name: "policy-1", + endpointLabels: ep1Labels, + destinationCIDRs: []string{destCIDR}, + podLabels: nodeGroup1Labels, + vtepIP: vtepIP1, + mac: mac1, + } + + addPolicy(t, k.policies, &policy1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{}) + + // Add a new endpoint & ID which matches policy-1 + ep1, id1 := newEndpointAndIdentity("ep-1", ep1IP, "", ep1Labels) + addEndpoint(t, k.endpoints, &ep1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep1IP, destCIDR, vtepIP1, mac1}, + }) + + // Update the endpoint labels in order for it to not be a match + id1 = updateEndpointAndIdentity(&ep1, id1, map[string]string{}) + addEndpoint(t, k.endpoints, &ep1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{}) + + // Restore the old endpoint lables in order for it to be a match + id1 = updateEndpointAndIdentity(&ep1, id1, ep1Labels) + addEndpoint(t, k.endpoints, &ep1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep1IP, destCIDR, vtepIP1, mac1}, + }) + + // Create a new policy + addPolicy(t, k.policies, &policyParams{ + name: "policy-2", + endpointLabels: ep2Labels, + destinationCIDRs: []string{destCIDR}, + podLabels: nodeGroup2Labels, + vtepIP: vtepIP2, + mac: mac2, + }) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep1IP, destCIDR, vtepIP1, mac1}, + }) + + // Add a new endpoint and ID which matches policy-2 + ep2, _ := newEndpointAndIdentity("ep-2", ep2IP, "", ep2Labels) + addEndpoint(t, k.endpoints, &ep2) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep1IP, destCIDR, vtepIP1, mac1}, + {ep2IP, destCIDR, vtepIP2, mac2}, + }) + + // Update the endpoint labels in order for it to not be a match + _ = updateEndpointAndIdentity(&ep1, id1, map[string]string{}) + addEndpoint(t, k.endpoints, &ep1) + waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep2IP, destCIDR, vtepIP2, mac2}, + }) +} + +func TestPrivilegedEndpointDataStore(t *testing.T) { + k := setupVtepPolicyTestSuite(t) + + createTestInterface(t, testInterface1, []string{egressCIDR1}) + + vtepPolicyManager := k.manager + policyMap := k.manager.policyMap + + k.policies.sync(t) + k.nodes.sync(t) + k.endpoints.sync(t) + + reconciliationEventsCount := vtepPolicyManager.reconciliationEventsCount.Load() + + node1 := newCiliumNode(node1, node1IP, nodeGroup1Labels) + k.nodes.process(t, resource.Event[*cilium_api_v2.CiliumNode]{ + Kind: resource.Upsert, + Object: node1.ToCiliumNode(), + }) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + // Create a new policy + policy1 := policyParams{ + name: "policy-1", + endpointLabels: ep1Labels, + destinationCIDRs: []string{destCIDR}, + podLabels: nodeGroup1Labels, + vtepIP: vtepIP1, + mac: mac1, + } + + addPolicy(t, k.policies, &policy1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{}) + + // Add a new endpoint & ID which matches policy-1 + ep1, _ := newEndpointAndIdentity("ep-1", ep1IP, "", ep1Labels) + addEndpoint(t, k.endpoints, &ep1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep1IP, destCIDR, vtepIP1, mac1}, + }) + + // Simulate statefulset pod migrations to a different node. + + // Produce a new endpoint ep2 similar to ep1 - with the same name & labels, but with a different IP address. + // The ep1 will be deleted. + ep2, _ := newEndpointAndIdentity(ep1.Name, ep2IP, "", ep1Labels) + + // Test event order: add new -> delete old + addEndpoint(t, k.endpoints, &ep2) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + deleteEndpoint(t, k.endpoints, &ep1) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep2IP, destCIDR, vtepIP1, mac1}, + }) + + // Produce a new endpoint ep3 similar to ep2 (and ep1) - with the same name & labels, but with a different IP address. + ep3, _ := newEndpointAndIdentity(ep1.Name, ep3IP, "", ep1Labels) + + // Test event order: delete old -> update new + deleteEndpoint(t, k.endpoints, &ep2) + reconciliationEventsCount = waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + addEndpoint(t, k.endpoints, &ep3) + waitForReconciliationRun(t, vtepPolicyManager, reconciliationEventsCount) + + assertVtepRules(t, policyMap, []vtepRule{ + {ep3IP, destCIDR, vtepIP1, mac1}, + }) +} + +func TestCell(t *testing.T) { + err := hive.New(Cell).Populate(hivetest.Logger(t)) + if err != nil { + t.Fatal(err) + } +} + +func createTestInterface(tb testing.TB, iface string, addrs []string) { + tb.Helper() + + la := netlink.NewLinkAttrs() + la.Name = iface + dummy := &netlink.Dummy{LinkAttrs: la} + if err := netlink.LinkAdd(dummy); err != nil { + tb.Fatal(err) + } + + link, err := safenetlink.LinkByName(iface) + if err != nil { + tb.Fatal(err) + } + + tb.Cleanup(func() { + if err := netlink.LinkDel(link); err != nil { + tb.Error(err) + } + }) + + if err := netlink.LinkSetUp(link); err != nil { + tb.Fatal(err) + } + + for _, addr := range addrs { + a, _ := netlink.ParseAddr(addr) + if err := netlink.AddrAdd(link, a); err != nil { + tb.Fatal(err) + } + } +} + +func waitForReconciliationRun(tb testing.TB, vtepPolicyManager *Manager, currentRun uint64) uint64 { + for range 100 { + count := vtepPolicyManager.reconciliationEventsCount.Load() + if count > currentRun { + return count + } + + // TODO: investigate why increasing the timeout was necessary to add IPv6 tests. + time.Sleep(30 * time.Millisecond) + } + + tb.Fatal("Reconciliation is taking too long to run") + return 0 +} + +func newCiliumNode(name, nodeIP string, nodeLabels map[string]string) nodeTypes.Node { + return nodeTypes.Node{ + Name: name, + Labels: nodeLabels, + IPAddresses: []nodeTypes.Address{ + { + Type: addressing.NodeInternalIP, + IP: netip.MustParseAddr(nodeIP).AsSlice(), + }, + }, + } +} + +// Mock the creation of endpoint and its corresponding identity, returns endpoint and ID. +func newEndpointAndIdentity(name, ipv4, ipv6 string, epLabels map[string]string) (k8sTypes.CiliumEndpoint, *identity.Identity) { + id, _, _ := identityAllocator.AllocateIdentity(context.Background(), labels.Map2Labels(epLabels, labels.LabelSourceK8s), true, identity.InvalidIdentity) + + return k8sTypes.CiliumEndpoint{ + ObjectMeta: slimv1.ObjectMeta{ + Name: name, + UID: types.UID(uuid.New().String()), + }, + Identity: &cilium_api_v2.EndpointIdentity{ + ID: int64(id.ID), + }, + Networking: &cilium_api_v2.EndpointNetworking{ + Addressing: cilium_api_v2.AddressPairList{ + &cilium_api_v2.AddressPair{ + IPV4: ipv4, + IPV6: ipv6, + }, + }, + }, + }, id +} + +// Mock the update of endpoint and its corresponding identity, with new labels. Returns new ID. +func updateEndpointAndIdentity(endpoint *k8sTypes.CiliumEndpoint, oldID *identity.Identity, newEpLabels map[string]string) *identity.Identity { + ctx := context.Background() + + identityAllocator.Release(ctx, oldID, true) + newID, _, _ := identityAllocator.AllocateIdentity(ctx, labels.Map2Labels(newEpLabels, labels.LabelSourceK8s), true, identity.InvalidIdentity) + endpoint.Identity.ID = int64(newID.ID) + return newID +} + +func parseVtepRule(sourceIP, destCIDR, vtepIP, vtepMAC string) parsedVtepRule { + sip := netip.MustParseAddr(sourceIP) + dc := netip.MustParsePrefix(destCIDR) + vip := netip.MustParseAddr(vtepIP) + vmac, _ := mac.ParseMAC(vtepMAC) + + return parsedVtepRule{ + sourceIP: sip, + destCIDR: dc, + vtepIP: vip, + vtepMAC: vmac, + } +} + +func assertVtepRules(t *testing.T, policyMap *vtep_policy.VtepPolicyMap, rules []vtepRule) { + t.Helper() + + err := tryAssertVtepRules(policyMap, rules) + require.NoError(t, err) +} + +func tryAssertVtepRules(policyMap *vtep_policy.VtepPolicyMap, rules []vtepRule) error { + parsedRules := []parsedVtepRule{} + for _, r := range rules { + parsedRules = append(parsedRules, parseVtepRule(r.sourceIP, r.destCIDR, r.vtepIP, r.vtepMAC)) + } + + for _, r := range parsedRules { + key := vtep_policy.NewKey(r.sourceIP, r.destCIDR) + + val, err := policyMap.Lookup(&key) + if err != nil { + return fmt.Errorf("cannot lookup policy entry: %w", err) + } + + if val == nil { + return fmt.Errorf("lookup successful but value is nil") + } + + if !val.Match(r.vtepIP, r.vtepMAC) { + return fmt.Errorf("mismatched val, wanted: %s %s, got: %s", r.vtepIP, r.vtepMAC, val) + } + } + + untrackedRule := false + policyMap.IterateWithCallback( + func(key *vtep_policy.VtepPolicyKey, val *vtep_policy.VtepPolicyVal) { + for _, r := range parsedRules { + if key.Match(r.sourceIP, r.destCIDR) && val.Match(r.vtepIP, r.vtepMAC) { + return + } + } + + untrackedRule = true + }) + + if untrackedRule { + return fmt.Errorf("Untracked vtep policy") + } + + return nil +} diff --git a/pkg/vteppolicy/min_duration.go b/pkg/vteppolicy/min_duration.go new file mode 100644 index 0000000000000..32ebdf508f245 --- /dev/null +++ b/pkg/vteppolicy/min_duration.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "github.com/cilium/cilium/pkg/time" +) + +// MinDuration represents a structure to manage timing intervals with a minimum duration and a time channel. +// It is not thread-safe, so it must be used only within a single goroutine. +type MinDuration struct { + c <-chan time.Time + minInterval time.Duration + lastCheck time.Time +} + +// NewMinDuration returns a new MinDuration instance if the given minInterval is positive. +func NewMinDuration(minInterval time.Duration) *MinDuration { + if minInterval <= 0 { + return nil + } + + if minInterval > 30*time.Second { + // It could be parametrized, but for now keep it simple. + minInterval = 30 * time.Second + } + + return &MinDuration{ + // Set it to -minInterval, so the first run should occur immediately. + // It could be parametrized, but for now keep it simple. + lastCheck: time.Now().Add(-minInterval), + minInterval: minInterval, + } +} + +// GetChannel returns the channel that fires when the required duration has passed since the last reconciliation, +// or nil if it is disabled. +// Nil channel can be used in select case statements, and it will not be fire +func (m *MinDuration) GetChannel() <-chan time.Time { + if m != nil { + return m.c + } + + return nil +} + +// Check returns true if: +// - this feature is disabled. +// - or the required duration has passed since the given last time. +func (m *MinDuration) Check() bool { + if m == nil { + return true + } + + t := time.Since(m.lastCheck) + if t < m.minInterval { + if m.c == nil { + m.c = time.After(m.minInterval - t) + } + } else { + // If time.After was created beforehand, then it is released now by the time.After function. + m.c = nil + } + + return m.c == nil +} + +// SetLastCheck sets the last check time to now. +// It should be called when an action is finished. +func (m *MinDuration) SetLastCheck() { + if m != nil { + m.lastCheck = time.Now() + } +} + +type retry struct { + timer *time.Timer +} + +func newRetry(d time.Duration) *retry { + return &retry{ + timer: time.NewTimer(d), + } +} + +func (r *retry) GetChannel() <-chan time.Time { + if r == nil || r.timer == nil { + return nil + } + + return r.timer.C +} + +func (r *retry) Stop() { + if r == nil || r.timer == nil { + return + } + + if !r.timer.Stop() { + // Drain channel if timer already fired + select { + case <-r.timer.C: + default: + } + } + + r.timer = nil +} diff --git a/pkg/vteppolicy/policy.go b/pkg/vteppolicy/policy.go new file mode 100644 index 0000000000000..416712b31605e --- /dev/null +++ b/pkg/vteppolicy/policy.go @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "fmt" + "net/netip" + + "k8s.io/apimachinery/pkg/types" + + k8sConst "github.com/cilium/cilium/pkg/k8s/apis/cilium.io" + "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" + "github.com/cilium/cilium/pkg/labels" + "github.com/cilium/cilium/pkg/mac" + "github.com/cilium/cilium/pkg/policy" + "github.com/cilium/cilium/pkg/policy/api" + policyTypes "github.com/cilium/cilium/pkg/policy/types" +) + +// vtepConfig is the gateway configuration derived at runtime from a policy. +type vtepConfig struct { + // vtepIP is the IP used for vxlan tunnel + vtepIP netip.Addr + // vtepMAC is the mac address of remote node behing vxlan tunnel + vtepMAC mac.MAC +} + +// PolicyConfig is the internal representation of CiliumVtepPolicy. +type PolicyConfig struct { + // id is the parsed config name and namespace + id types.NamespacedName + + podSelectors []*policyTypes.LabelSelector + dstCIDRs []netip.Prefix + + matchedEndpoints map[endpointID]*endpointMetadata + vtepConfig vtepConfig +} + +// PolicyID includes policy name and namespace +type policyID = types.NamespacedName + +// matchesNodeLabels determines if the given node lables is a match for the +// policy config based on matching labels. +func (config *PolicyConfig) matchesPodLabels(podLabels map[string]string) bool { + labelsToMatch := labels.K8sSet(podLabels) + for i := range config.podSelectors { + if policyTypes.Matches(config.podSelectors[i], labelsToMatch) { + return true + } + } + return false +} + +// updateMatchedEndpointIDs update the policy's cache of matched endpoint IDs +func (config *PolicyConfig) updateMatchedEndpointIDs(epDataStore map[endpointID]*endpointMetadata) { + config.matchedEndpoints = make(map[endpointID]*endpointMetadata) + for _, endpoint := range epDataStore { + if config.matchesPodLabels(endpoint.labels) { + config.matchedEndpoints[endpoint.id] = endpoint + } + } +} + +// forEachEndpointAndCIDR iterates through each combination of endpoints and +// destination/excluded CIDRs of the receiver policy, and for each of them it +// calls the f callback function passing the given endpoint and CIDR, together +// with a boolean value indicating if the CIDR belongs to the excluded ones and +// the vtepConfig of the receiver policy +func (config *PolicyConfig) forEachEndpointAndCIDR(f func(netip.Addr, netip.Prefix, *vtepConfig)) { + for _, endpoint := range config.matchedEndpoints { + for _, endpointIP := range endpoint.ips { + for _, dstCIDR := range config.dstCIDRs { + f(endpointIP, dstCIDR, &config.vtepConfig) + } + } + } +} + +// ParseCVP takes a CiliumVtepPolicy CR and converts to PolicyConfig, +// the internal representation of the vtep policy +func ParseCVP(cvp *v2alpha1.CiliumVtepPolicy) (*PolicyConfig, error) { + var podSelectorList []*policyTypes.LabelSelector + var dstCidrList []netip.Prefix + var vtepIP netip.Addr + + allowAllNamespacesRequirement := slim_metav1.LabelSelectorRequirement{ + Key: k8sConst.PodNamespaceLabel, + Operator: slim_metav1.LabelSelectorOpExists, + } + + name := cvp.ObjectMeta.Name + if name == "" { + return nil, fmt.Errorf("must have a name") + } + + destinationCIDRs := cvp.Spec.DestinationCIDRs + if destinationCIDRs == nil { + return nil, fmt.Errorf("destinationCIDRs can't be empty") + } + + externalVTEP := cvp.Spec.ExternalVTEP + if externalVTEP == nil { + return nil, fmt.Errorf("externalVTEP can't be empty") + } + + vtepIP, err := netip.ParseAddr(externalVTEP.IP) + if err != nil { + return nil, fmt.Errorf("cannot parse vtep ip") + } + + vtepMAC, err := mac.ParseMAC(string(externalVTEP.MAC)) + if err != nil { + return nil, fmt.Errorf("cannot parse vtep mac %s: %w", vtepMAC, err) + } + + for _, cidrString := range destinationCIDRs { + cidr, err := netip.ParsePrefix(string(cidrString)) + if err != nil { + return nil, fmt.Errorf("failed to parse destination CIDR %s: %w", cidrString, err) + } + dstCidrList = append(dstCidrList, cidr) + } + + for _, vtepRule := range cvp.Spec.Selectors { + if vtepRule.NamespaceSelector != nil { + prefixedNsSelector := vtepRule.NamespaceSelector + matchLabels := map[string]string{} + // We use our own special label prefix for namespace metadata, + // thus we need to prefix that prefix to all NamespaceSelector.MatchLabels + for k, v := range vtepRule.NamespaceSelector.MatchLabels { + matchLabels[policy.JoinPath(k8sConst.PodNamespaceMetaLabels, k)] = v + } + + prefixedNsSelector.MatchLabels = matchLabels + + // We use our own special label prefix for namespace metadata, + // thus we need to prefix that prefix to all NamespaceSelector.MatchLabels + for i, lsr := range vtepRule.NamespaceSelector.MatchExpressions { + lsr.Key = policy.JoinPath(k8sConst.PodNamespaceMetaLabels, lsr.Key) + prefixedNsSelector.MatchExpressions[i] = lsr + } + + // Empty namespace selector selects all namespaces (i.e., a namespace + // label exists). + if len(vtepRule.NamespaceSelector.MatchLabels) == 0 && len(vtepRule.NamespaceSelector.MatchExpressions) == 0 { + prefixedNsSelector.MatchExpressions = []slim_metav1.LabelSelectorRequirement{allowAllNamespacesRequirement} + } + } else if vtepRule.PodSelector != nil { + podSelectorList = append( + podSelectorList, + policyTypes.NewLabelSelector(api.NewESFromK8sLabelSelector(labels.LabelSourceK8sKeyPrefix, vtepRule.PodSelector))) + } else { + return nil, fmt.Errorf("cannot have both nil namespace selector and nil pod selector") + } + } + + return &PolicyConfig{ + podSelectors: podSelectorList, + dstCIDRs: dstCidrList, + vtepConfig: vtepConfig{ + vtepIP: vtepIP, + vtepMAC: vtepMAC, + }, + matchedEndpoints: make(map[endpointID]*endpointMetadata), + id: types.NamespacedName{ + Name: name, + }, + }, nil +} + +// ParseCEGPConfigID takes a CiliumVtepPolicy CR and returns only the config id +func ParseCVPConfigID(cvp *v2alpha1.CiliumVtepPolicy) types.NamespacedName { + return policyID{ + Name: cvp.Name, + } +} diff --git a/pkg/vteppolicy/policy_test.go b/pkg/vteppolicy/policy_test.go new file mode 100644 index 0000000000000..0722e8ddec8be --- /dev/null +++ b/pkg/vteppolicy/policy_test.go @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/types" + + slimv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" + "github.com/cilium/cilium/pkg/labels" + "github.com/cilium/cilium/pkg/policy/api" + policyTypes "github.com/cilium/cilium/pkg/policy/types" +) + +func getAsPolicyLabelSelectors(k8sLss []api.EndpointSelector) (lss []*policyTypes.LabelSelector) { + for _, ls := range k8sLss { + lss = append(lss, policyTypes.NewLabelSelector( + api.NewESFromK8sLabelSelector(labels.LabelSourceK8sKeyPrefix, ls.LabelSelector)), + ) + } + return lss +} + +func TestPolicyConfig_updateMatchedEndpointIDs(t *testing.T) { + type fields struct { + id types.NamespacedName + endpointSelectors []api.EndpointSelector + podSelectors []api.EndpointSelector + dstCIDRs []netip.Prefix + matchedEndpoints map[endpointID]*endpointMetadata + } + type args struct { + epDataStore map[endpointID]*endpointMetadata + } + tests := []struct { + name string + fields fields + args args + want int + wantEndpointID endpointID + }{ + { + name: "Test updateMatchedEndpointIDs with endpoints", + fields: fields{ + id: types.NamespacedName{ + Name: "test", + }, + endpointSelectors: []api.EndpointSelector{ + { + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, + }, + podSelectors: []api.EndpointSelector{ + { + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, + }, + }, + args: args{ + epDataStore: map[endpointID]*endpointMetadata{ + "123456": { + id: "123456", + labels: map[string]string{ + "app": "test", + }, + }, + }, + }, + want: 1, + wantEndpointID: endpointID("123456"), + }, + { + name: "Test updateMatchedEndpointIDs endpoints with no match", + fields: fields{ + id: types.NamespacedName{ + Name: "test", + }, + endpointSelectors: []api.EndpointSelector{ + { + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, + }, + podSelectors: []api.EndpointSelector{ + { + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: map[string]string{ + "pod-name": "pod1", + }, + }, + }, + }, + }, + args: args{ + epDataStore: map[endpointID]*endpointMetadata{ + "123456": { + id: "123456", + labels: map[string]string{ + "app": "test", + }, + }, + }, + }, + want: 0, + wantEndpointID: "", + }, + { + name: "Test updateMatchedEndpointIDs endpoints with no match label", + fields: fields{ + id: types.NamespacedName{ + Name: "test", + }, + endpointSelectors: []api.EndpointSelector{ + { + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "test", + }, + }, + }, + }, + podSelectors: []api.EndpointSelector{ + { + LabelSelector: &slimv1.LabelSelector{ + MatchLabels: map[string]string{ + "pod-name": "pod1", + }, + }, + }, + }, + }, + args: args{ + epDataStore: map[endpointID]*endpointMetadata{ + "123456": { + id: "123456", + labels: map[string]string{ + "app": "test", + }, + }, + }, + }, + want: 0, + wantEndpointID: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := &PolicyConfig{ + id: tt.fields.id, + podSelectors: getAsPolicyLabelSelectors(tt.fields.podSelectors), + dstCIDRs: tt.fields.dstCIDRs, + matchedEndpoints: tt.fields.matchedEndpoints, + } + config.updateMatchedEndpointIDs(tt.args.epDataStore) + assert.Len(t, config.matchedEndpoints, tt.want) + if tt.want > 0 { + assert.Contains(t, config.matchedEndpoints, endpointID(tt.wantEndpointID)) + } + }) + } +} diff --git a/pkg/vteppolicy/resource.go b/pkg/vteppolicy/resource.go new file mode 100644 index 0000000000000..eb44894e9a697 --- /dev/null +++ b/pkg/vteppolicy/resource.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Cilium + +package vteppolicy + +import ( + "github.com/cilium/hive/cell" + + "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" + "github.com/cilium/cilium/pkg/k8s/client" + "github.com/cilium/cilium/pkg/k8s/resource" + "github.com/cilium/cilium/pkg/k8s/utils" +) + +type Policy = v2alpha1.CiliumVtepPolicy + +func newPolicyResource(lc cell.Lifecycle, c client.Clientset) resource.Resource[*Policy] { + if !c.IsEnabled() { + return nil + } + lw := utils.ListerWatcherFromTyped(c.CiliumV2alpha1().CiliumVtepPolicies()) + return resource.New[*Policy](lc, lw, nil) +}