From 27baf2917e8d69a80f5baca2394019cf79c6bb19 Mon Sep 17 00:00:00 2001 From: "Mohamed S. Mahmoud" <mmahmoud@redhat.com> Date: Sat, 24 Jun 2023 09:33:46 -0400 Subject: [PATCH] NETOBSERV-1061: Add TCP drop and DNS tracking hooks (#115) * Add TCP drop hook and update flows metrics Signed-off-by: msherif1234 <mmahmoud@redhat.com> * update agent e2e manifest to mount kernel debug volume Signed-off-by: msherif1234 <mmahmoud@redhat.com> * Add light weight DNS tracker hook Signed-off-by: msherif1234 <mmahmoud@redhat.com> * rename tcpdrop fields to reflect its the latest drop fix lint errors flatten icmp block Signed-off-by: msherif1234 <mmahmoud@redhat.com> --------- Signed-off-by: msherif1234 <mmahmoud@redhat.com> --- bpf/configs.h | 9 + bpf/dns_tracker.h | 103 ++++ bpf/flow.h | 22 + bpf/flows.c | 223 +------ bpf/maps_definition.h | 21 + bpf/tcp_drops.h | 88 +++ bpf/utils.h | 295 +++++++++ e2e/cluster/base/04-agent.yml | 9 + e2e/ipfix/manifests/30-agent.yml | 9 + e2e/kafka/manifests/30-agent.yml | 9 + .../server/flowlogs-dump-collector.go | 20 +- pkg/agent/agent.go | 2 +- pkg/agent/config.go | 4 + pkg/ebpf/bpf_bpfeb.go | 23 + pkg/ebpf/bpf_bpfeb.o | Bin 27208 -> 67240 bytes pkg/ebpf/bpf_bpfel.go | 23 + pkg/ebpf/bpf_bpfel.o | Bin 27280 -> 67888 bytes pkg/ebpf/tracer.go | 67 +- pkg/exporter/kafka_proto_test.go | 2 +- pkg/exporter/proto.go | 80 ++- pkg/flow/account.go | 2 +- pkg/flow/account_test.go | 72 ++- pkg/flow/record.go | 24 +- pkg/flow/record_test.go | 25 +- pkg/flow/tracer_map.go | 2 +- pkg/grpc/grpc_test.go | 35 +- pkg/pbflow/flow.pb.go | 283 +++++---- proto/flow.proto | 17 +- vendor/github.com/cilium/ebpf/link/cgroup.go | 165 +++++ vendor/github.com/cilium/ebpf/link/doc.go | 2 + vendor/github.com/cilium/ebpf/link/iter.go | 85 +++ vendor/github.com/cilium/ebpf/link/kprobe.go | 574 ++++++++++++++++++ .../cilium/ebpf/link/kprobe_multi.go | 180 ++++++ vendor/github.com/cilium/ebpf/link/link.go | 315 ++++++++++ vendor/github.com/cilium/ebpf/link/netns.go | 36 ++ .../github.com/cilium/ebpf/link/perf_event.go | 434 +++++++++++++ .../github.com/cilium/ebpf/link/platform.go | 25 + vendor/github.com/cilium/ebpf/link/program.go | 76 +++ vendor/github.com/cilium/ebpf/link/query.go | 63 ++ .../cilium/ebpf/link/raw_tracepoint.go | 87 +++ .../cilium/ebpf/link/socket_filter.go | 40 ++ .../github.com/cilium/ebpf/link/syscalls.go | 123 ++++ .../github.com/cilium/ebpf/link/tracepoint.go | 77 +++ vendor/github.com/cilium/ebpf/link/tracing.go | 150 +++++ vendor/github.com/cilium/ebpf/link/uprobe.go | 359 +++++++++++ vendor/github.com/cilium/ebpf/link/xdp.go | 54 ++ vendor/modules.txt | 1 + 47 files changed, 3891 insertions(+), 424 deletions(-) create mode 100644 bpf/configs.h create mode 100644 bpf/dns_tracker.h create mode 100644 bpf/maps_definition.h create mode 100644 bpf/tcp_drops.h create mode 100644 bpf/utils.h create mode 100644 vendor/github.com/cilium/ebpf/link/cgroup.go create mode 100644 vendor/github.com/cilium/ebpf/link/doc.go create mode 100644 vendor/github.com/cilium/ebpf/link/iter.go create mode 100644 vendor/github.com/cilium/ebpf/link/kprobe.go create mode 100644 vendor/github.com/cilium/ebpf/link/kprobe_multi.go create mode 100644 vendor/github.com/cilium/ebpf/link/link.go create mode 100644 vendor/github.com/cilium/ebpf/link/netns.go create mode 100644 vendor/github.com/cilium/ebpf/link/perf_event.go create mode 100644 vendor/github.com/cilium/ebpf/link/platform.go create mode 100644 vendor/github.com/cilium/ebpf/link/program.go create mode 100644 vendor/github.com/cilium/ebpf/link/query.go create mode 100644 vendor/github.com/cilium/ebpf/link/raw_tracepoint.go create mode 100644 vendor/github.com/cilium/ebpf/link/socket_filter.go create mode 100644 vendor/github.com/cilium/ebpf/link/syscalls.go create mode 100644 vendor/github.com/cilium/ebpf/link/tracepoint.go create mode 100644 vendor/github.com/cilium/ebpf/link/tracing.go create mode 100644 vendor/github.com/cilium/ebpf/link/uprobe.go create mode 100644 vendor/github.com/cilium/ebpf/link/xdp.go diff --git a/bpf/configs.h b/bpf/configs.h new file mode 100644 index 000000000..208faea1f --- /dev/null +++ b/bpf/configs.h @@ -0,0 +1,9 @@ + +#ifndef __CONFIGS_H__ +#define __CONFIGS_H__ + +// Constant definitions, to be overridden by the invoker +volatile const u32 sampling = 0; +volatile const u8 trace_messages = 0; + +#endif //__CONFIGS_H__ diff --git a/bpf/dns_tracker.h b/bpf/dns_tracker.h new file mode 100644 index 000000000..f3aa795f3 --- /dev/null +++ b/bpf/dns_tracker.h @@ -0,0 +1,103 @@ +/* + light weight DNS tracker using trace points. +*/ + +#ifndef __DNS_TRACKER_H__ +#define __DNS_TRACKER_H__ +#include "utils.h" + +#define DNS_PORT 53 +#define DNS_QR_FLAG 0x8000 +#define UDP_MAXMSG 512 + +struct dns_header { + u16 id; + u16 flags; + u16 qdcount; + u16 ancount; + u16 nscount; + u16 arcount; +}; + +static inline void find_or_create_dns_flow(flow_id *id, struct dns_header *dns, int len, int dir, u16 flags) { + flow_metrics *aggregate_flow = bpf_map_lookup_elem(&aggregated_flows, id); + u64 current_time = bpf_ktime_get_ns(); + // net_dev_queue trace point hook will run before TC hooks, so the flow shouldn't exists, if it does + // that indicates we have a stale DNS query/response or in the middle of TCP flow so we will do nothing + if (aggregate_flow == NULL) { + // there is no matching flows so lets create new one and add the drops + flow_metrics new_flow; + __builtin_memset(&new_flow, 0, sizeof(new_flow)); + new_flow.start_mono_time_ts = current_time; + new_flow.end_mono_time_ts = current_time; + new_flow.packets = 1; + new_flow.bytes = len; + new_flow.flags = flags; + new_flow.dns_record.id = bpf_ntohs(dns->id); + new_flow.dns_record.flags = bpf_ntohs(dns->flags); + if (dir == EGRESS) { + new_flow.dns_record.req_mono_time_ts = current_time; + } else { + new_flow.dns_record.rsp_mono_time_ts = current_time; + } + bpf_map_update_elem(&aggregated_flows, id, &new_flow, BPF_ANY); + } +} + +static inline int trace_dns(struct sk_buff *skb) { + flow_id id; + u8 protocol = 0; + u16 family = 0,flags = 0, len = 0; + + __builtin_memset(&id, 0, sizeof(id)); + + id.if_index = skb->skb_iif; + + // read L2 info + set_key_with_l2_info(skb, &id, &family); + + // read L3 info + set_key_with_l3_info(skb, family, &id, &protocol); + + switch (protocol) { + case IPPROTO_UDP: + len = set_key_with_udp_info(skb, &id, IPPROTO_UDP); + // make sure udp payload doesn't exceed max msg size + if (len - sizeof(struct udphdr) > UDP_MAXMSG) { + return -1; + } + // set the length to udp hdr size as it will be used below to locate dns header + len = sizeof(struct udphdr); + break; + case IPPROTO_TCP: + len = set_key_with_tcp_info(skb, &id, IPPROTO_TCP, &flags); + break; + default: + return -1; + } + + // check for DNS packets + if (id.dst_port == DNS_PORT || id.src_port == DNS_PORT) { + struct dns_header dns; + bpf_probe_read(&dns, sizeof(dns), (struct dns_header *)(skb->head + skb->transport_header + len)); + if ((bpf_ntohs(dns.flags) & DNS_QR_FLAG) == 0) { /* dns query */ + id.direction = EGRESS; + } else { /* dns response */ + id.direction = INGRESS; + } // end of dns response + find_or_create_dns_flow(&id, &dns, skb->len, id.direction, flags); + } // end of dns port check + + return 0; +} + +SEC("tracepoint/net/net_dev_queue") +int trace_net_packets(struct trace_event_raw_net_dev_template *args) { + struct sk_buff skb; + + __builtin_memset(&skb, 0, sizeof(skb)); + bpf_probe_read(&skb, sizeof(struct sk_buff), args->skbaddr); + return trace_dns(&skb); +} + +#endif // __DNS_TRACKER_H__ diff --git a/bpf/flow.h b/bpf/flow.h index 366a686b7..ddb306f8e 100644 --- a/bpf/flow.h +++ b/bpf/flow.h @@ -10,6 +10,8 @@ typedef __u16 u16; typedef __u32 u32; typedef __u64 u64; +#define AF_INET 2 +#define AF_INET6 10 #define ETH_ALEN 6 #define ETH_P_IP 0x0800 #define ETH_P_IPV6 0x86DD @@ -30,8 +32,24 @@ typedef struct flow_metrics_t { // 0 otherwise // https://chromium.googlesource.com/chromiumos/docs/+/master/constants/errnos.md u8 errno; + struct tcp_drops_t { + u32 packets; + u64 bytes; + u16 latest_flags; + u8 latest_state; + u32 latest_drop_cause; + } __attribute__((packed)) tcp_drops; + struct dns_record_t { + u16 id; + u16 flags; + u64 req_mono_time_ts; + u64 rsp_mono_time_ts; + } __attribute__((packed)) dns_record; } __attribute__((packed)) flow_metrics; +// Force emitting struct tcp_drops into the ELF. +const struct tcp_drops_t *unused0 __attribute__((unused)); + // Force emitting struct flow_metrics into the ELF. const struct flow_metrics_t *unused1 __attribute__((unused)); @@ -71,4 +89,8 @@ typedef struct flow_record_t { // Force emitting struct flow_record into the ELF. const struct flow_record_t *unused3 __attribute__((unused)); + +// Force emitting struct dns_record into the ELF. +const struct dns_record_t *unused4 __attribute__((unused)); + #endif diff --git a/bpf/flows.c b/bpf/flows.c index ba934557f..5219b808d 100644 --- a/bpf/flows.c +++ b/bpf/flows.c @@ -13,226 +13,10 @@ until an entry is available. 4) When hash collision is detected, we send the new entry to userpace via ringbuffer. */ -#include <vmlinux.h> -#include <bpf_helpers.h> +#include "utils.h" +#include "tcp_drops.h" +#include "dns_tracker.h" -#include "flow.h" -#define DISCARD 1 -#define SUBMIT 0 - -// according to field 61 in https://www.iana.org/assignments/ipfix/ipfix.xhtml -#define INGRESS 0 -#define EGRESS 1 - -// Flags according to RFC 9293 & https://www.iana.org/assignments/ipfix/ipfix.xhtml -#define FIN_FLAG 0x01 -#define SYN_FLAG 0x02 -#define RST_FLAG 0x04 -#define PSH_FLAG 0x08 -#define ACK_FLAG 0x10 -#define URG_FLAG 0x20 -#define ECE_FLAG 0x40 -#define CWR_FLAG 0x80 -// Custom flags exported -#define SYN_ACK_FLAG 0x100 -#define FIN_ACK_FLAG 0x200 -#define RST_ACK_FLAG 0x400 - -#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \ - __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ -#define bpf_ntohs(x) __builtin_bswap16(x) -#define bpf_htons(x) __builtin_bswap16(x) -#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && \ - __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ -#define bpf_ntohs(x) (x) -#define bpf_htons(x) (x) -#else -# error "Endianness detection needs to be set up for your compiler?!" -#endif - -// Common Ringbuffer as a conduit for ingress/egress flows to userspace -struct { - __uint(type, BPF_MAP_TYPE_RINGBUF); - __uint(max_entries, 1 << 24); -} direct_flows SEC(".maps"); - -// Key: the flow identifier. Value: the flow metrics for that identifier. -struct { - __uint(type, BPF_MAP_TYPE_HASH); - __type(key, flow_id); - __type(value, flow_metrics); - __uint(max_entries, 1 << 24); - __uint(map_flags, BPF_F_NO_PREALLOC); -} aggregated_flows SEC(".maps"); - -// Constant definitions, to be overridden by the invoker -volatile const u32 sampling = 0; -volatile const u8 trace_messages = 0; - -const u8 ip4in6[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; - -// sets the TCP header flags for connection information -static inline void set_flags(struct tcphdr *th, u16 *flags) { - //If both ACK and SYN are set, then it is server -> client communication during 3-way handshake. - if (th->ack && th->syn) { - *flags |= SYN_ACK_FLAG; - } else if (th->ack && th->fin ) { - // If both ACK and FIN are set, then it is graceful termination from server. - *flags |= FIN_ACK_FLAG; - } else if (th->ack && th->rst ) { - // If both ACK and RST are set, then it is abrupt connection termination. - *flags |= RST_ACK_FLAG; - } else if (th->fin) { - *flags |= FIN_FLAG; - } else if (th->syn) { - *flags |= SYN_FLAG; - } else if (th->ack) { - *flags |= ACK_FLAG; - } else if (th->rst) { - *flags |= RST_FLAG; - } else if (th->psh) { - *flags |= PSH_FLAG; - } else if (th->urg) { - *flags |= URG_FLAG; - } else if (th->ece) { - *flags |= ECE_FLAG; - } else if (th->cwr) { - *flags |= CWR_FLAG; - } -} - -// L4_info structure contains L4 headers parsed information. -struct l4_info_t { - // TCP/UDP/SCTP source port in host byte order - u16 src_port; - // TCP/UDP/SCTP destination port in host byte order - u16 dst_port; - // ICMPv4/ICMPv6 type value - u8 icmp_type; - // ICMPv4/ICMPv6 code value - u8 icmp_code; - // TCP flags - u16 flags; -}; - -// Extract L4 info for the supported protocols -static inline void fill_l4info(void *l4_hdr_start, void *data_end, u8 protocol, - struct l4_info_t *l4_info) { - switch (protocol) { - case IPPROTO_TCP: { - struct tcphdr *tcp = l4_hdr_start; - if ((void *)tcp + sizeof(*tcp) <= data_end) { - l4_info->src_port = bpf_ntohs(tcp->source); - l4_info->dst_port = bpf_ntohs(tcp->dest); - set_flags(tcp, &l4_info->flags); - } - } break; - case IPPROTO_UDP: { - struct udphdr *udp = l4_hdr_start; - if ((void *)udp + sizeof(*udp) <= data_end) { - l4_info->src_port = bpf_ntohs(udp->source); - l4_info->dst_port = bpf_ntohs(udp->dest); - } - } break; - case IPPROTO_SCTP: { - struct sctphdr *sctph = l4_hdr_start; - if ((void *)sctph + sizeof(*sctph) <= data_end) { - l4_info->src_port = bpf_ntohs(sctph->source); - l4_info->dst_port = bpf_ntohs(sctph->dest); - } - } break; - case IPPROTO_ICMP: { - struct icmphdr *icmph = l4_hdr_start; - if ((void *)icmph + sizeof(*icmph) <= data_end) { - l4_info->icmp_type = icmph->type; - l4_info->icmp_code = icmph->code; - } - } break; - case IPPROTO_ICMPV6: { - struct icmp6hdr *icmp6h = l4_hdr_start; - if ((void *)icmp6h + sizeof(*icmp6h) <= data_end) { - l4_info->icmp_type = icmp6h->icmp6_type; - l4_info->icmp_code = icmp6h->icmp6_code; - } - } break; - default: - break; - } -} - -// sets flow fields from IPv4 header information -static inline int fill_iphdr(struct iphdr *ip, void *data_end, flow_id *id, u16 *flags) { - struct l4_info_t l4_info; - void *l4_hdr_start; - - l4_hdr_start = (void *)ip + sizeof(*ip); - if (l4_hdr_start > data_end) { - return DISCARD; - } - __builtin_memset(&l4_info, 0, sizeof(l4_info)); - __builtin_memcpy(id->src_ip, ip4in6, sizeof(ip4in6)); - __builtin_memcpy(id->dst_ip, ip4in6, sizeof(ip4in6)); - __builtin_memcpy(id->src_ip + sizeof(ip4in6), &ip->saddr, sizeof(ip->saddr)); - __builtin_memcpy(id->dst_ip + sizeof(ip4in6), &ip->daddr, sizeof(ip->daddr)); - id->transport_protocol = ip->protocol; - fill_l4info(l4_hdr_start, data_end, ip->protocol, &l4_info); - id->src_port = l4_info.src_port; - id->dst_port = l4_info.dst_port; - id->icmp_type = l4_info.icmp_type; - id->icmp_code = l4_info.icmp_code; - *flags = l4_info.flags; - - return SUBMIT; -} - -// sets flow fields from IPv6 header information -static inline int fill_ip6hdr(struct ipv6hdr *ip, void *data_end, flow_id *id, u16 *flags) { - struct l4_info_t l4_info; - void *l4_hdr_start; - - l4_hdr_start = (void *)ip + sizeof(*ip); - if (l4_hdr_start > data_end) { - return DISCARD; - } - __builtin_memset(&l4_info, 0, sizeof(l4_info)); - __builtin_memcpy(id->src_ip, ip->saddr.in6_u.u6_addr8, 16); - __builtin_memcpy(id->dst_ip, ip->daddr.in6_u.u6_addr8, 16); - id->transport_protocol = ip->nexthdr; - fill_l4info(l4_hdr_start, data_end, ip->nexthdr, &l4_info); - id->src_port = l4_info.src_port; - id->dst_port = l4_info.dst_port; - id->icmp_type = l4_info.icmp_type; - id->icmp_code = l4_info.icmp_code; - *flags = l4_info.flags; - - return SUBMIT; -} -// sets flow fields from Ethernet header information -static inline int fill_ethhdr(struct ethhdr *eth, void *data_end, flow_id *id, u16 *flags) { - if ((void *)eth + sizeof(*eth) > data_end) { - return DISCARD; - } - __builtin_memcpy(id->dst_mac, eth->h_dest, ETH_ALEN); - __builtin_memcpy(id->src_mac, eth->h_source, ETH_ALEN); - id->eth_protocol = bpf_ntohs(eth->h_proto); - - if (id->eth_protocol == ETH_P_IP) { - struct iphdr *ip = (void *)eth + sizeof(*eth); - return fill_iphdr(ip, data_end, id, flags); - } else if (id->eth_protocol == ETH_P_IPV6) { - struct ipv6hdr *ip6 = (void *)eth + sizeof(*eth); - return fill_ip6hdr(ip6, data_end, id, flags); - } else { - // TODO : Need to implement other specific ethertypes if needed - // For now other parts of flow id remain zero - __builtin_memset(&(id->src_ip), 0, sizeof(struct in6_addr)); - __builtin_memset(&(id->dst_ip), 0, sizeof(struct in6_addr)); - id->transport_protocol = 0; - id->src_port = 0; - id->dst_port = 0; - } - return SUBMIT; -} static inline int flow_monitor(struct __sk_buff *skb, u8 direction) { // If sampling is defined, will only parse 1 out of "sampling" flows @@ -317,4 +101,5 @@ SEC("tc_egress") int egress_flow_parse(struct __sk_buff *skb) { return flow_monitor(skb, EGRESS); } + char _license[] SEC("license") = "GPL"; diff --git a/bpf/maps_definition.h b/bpf/maps_definition.h new file mode 100644 index 000000000..8bd0d0120 --- /dev/null +++ b/bpf/maps_definition.h @@ -0,0 +1,21 @@ +#ifndef __MAPS_DEFINITION_H__ +#define __MAPS_DEFINITION_H__ + +#include <vmlinux.h> + +// Common Ringbuffer as a conduit for ingress/egress flows to userspace +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 1 << 24); +} direct_flows SEC(".maps"); + +// Key: the flow identifier. Value: the flow metrics for that identifier. +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, flow_id); + __type(value, flow_metrics); + __uint(max_entries, 1 << 24); + __uint(map_flags, BPF_F_NO_PREALLOC); +} aggregated_flows SEC(".maps"); + +#endif //__MAPS_DEFINITION_H__ diff --git a/bpf/tcp_drops.h b/bpf/tcp_drops.h new file mode 100644 index 000000000..bbd84b547 --- /dev/null +++ b/bpf/tcp_drops.h @@ -0,0 +1,88 @@ +/* + TCPDrops using trace points. +*/ + +#ifndef __TCP_DROPS_H__ +#define __TCP_DROPS_H__ + +#include "utils.h" + +static inline int trace_tcp_drop(void *ctx, struct sock *sk, + struct sk_buff *skb, + enum skb_drop_reason reason) { + if (sk == NULL) + return 0; + + flow_id id; + __builtin_memset(&id, 0, sizeof(id)); + + u8 state = 0, protocol = 0; + u16 family = 0,flags = 0; + + // pull in details from the packet headers and the sock struct + bpf_probe_read(&state, sizeof(u8), (u8 *)&sk->__sk_common.skc_state); + + id.if_index = skb->skb_iif; + + // read L2 info + set_key_with_l2_info(skb, &id, &family); + + // read L3 info + set_key_with_l3_info(skb, family, &id, &protocol); + + // We only support TCP drops for any other protocol just return w/o doing anything + if (protocol != IPPROTO_TCP) { + return 0; + } + + // read L4 info + set_key_with_tcp_info(skb, &id, protocol, &flags); + + long ret = 0; + for (direction_t dir = INGRESS; dir < MAX_DIRECTION; dir++) { + id.direction = dir; + ret = tcp_drop_lookup_and_update_flow(skb, &id, state, flags, reason); + if (ret == 0) { + return 0; + } + } + // there is no matching flows so lets create new one and add the drops + u64 current_time = bpf_ktime_get_ns(); + id.direction = INGRESS; + flow_metrics new_flow = { + .start_mono_time_ts = current_time, + .end_mono_time_ts = current_time, + .flags = flags, + .tcp_drops.packets = 1, + .tcp_drops.bytes = skb->len, + .tcp_drops.latest_state = state, + .tcp_drops.latest_flags = flags, + .tcp_drops.latest_drop_cause = reason, + }; + ret = bpf_map_update_elem(&aggregated_flows, &id, &new_flow, BPF_ANY); + if (trace_messages && ret != 0) { + bpf_printk("error tcp drop creating new flow %d\n", ret); + } + + return ret; +} + +SEC("tracepoint/skb/kfree_skb") +int kfree_skb(struct trace_event_raw_kfree_skb *args) { + struct sk_buff skb; + __builtin_memset(&skb, 0, sizeof(skb)); + + bpf_probe_read(&skb, sizeof(struct sk_buff), args->skbaddr); + struct sock *sk = skb.sk; + enum skb_drop_reason reason = args->reason; + + // SKB_NOT_DROPPED_YET, + // SKB_CONSUMED, + // SKB_DROP_REASON_NOT_SPECIFIED, + if (reason > SKB_DROP_REASON_NOT_SPECIFIED) { + return trace_tcp_drop(args, sk, &skb, reason); + } + return 0; +} + +#endif //__TCP_DROPS_H__ diff --git a/bpf/utils.h b/bpf/utils.h new file mode 100644 index 000000000..5338f1b72 --- /dev/null +++ b/bpf/utils.h @@ -0,0 +1,295 @@ +#ifndef __UTILS_H__ +#define __UTILS_H__ + +#include <vmlinux.h> +#include <bpf_helpers.h> + +#include "flow.h" +#include "maps_definition.h" +#include "configs.h" + +#define DISCARD 1 +#define SUBMIT 0 + +// according to field 61 in https://www.iana.org/assignments/ipfix/ipfix.xhtml +typedef enum { + INGRESS = 0, + EGRESS = 1, + MAX_DIRECTION = 2, +} direction_t; + +// L4_info structure contains L4 headers parsed information. +struct l4_info_t { + // TCP/UDP/SCTP source port in host byte order + u16 src_port; + // TCP/UDP/SCTP destination port in host byte order + u16 dst_port; + // ICMPv4/ICMPv6 type value + u8 icmp_type; + // ICMPv4/ICMPv6 code value + u8 icmp_code; + // TCP flags + u16 flags; +}; + +const u8 ip4in6[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + +// Flags according to RFC 9293 & https://www.iana.org/assignments/ipfix/ipfix.xhtml +#define FIN_FLAG 0x01 +#define SYN_FLAG 0x02 +#define RST_FLAG 0x04 +#define PSH_FLAG 0x08 +#define ACK_FLAG 0x10 +#define URG_FLAG 0x20 +#define ECE_FLAG 0x40 +#define CWR_FLAG 0x80 +// Custom flags exported +#define SYN_ACK_FLAG 0x100 +#define FIN_ACK_FLAG 0x200 +#define RST_ACK_FLAG 0x400 + +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && \ + __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +#define bpf_ntohs(x) __builtin_bswap16(x) +#define bpf_htons(x) __builtin_bswap16(x) +#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && \ + __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define bpf_ntohs(x) (x) +#define bpf_htons(x) (x) +#else +# error "Endianness detection needs to be set up for your compiler?!" +#endif + + +// sets the TCP header flags for connection information +static inline void set_flags(struct tcphdr *th, u16 *flags) { + //If both ACK and SYN are set, then it is server -> client communication during 3-way handshake. + if (th->ack && th->syn) { + *flags |= SYN_ACK_FLAG; + } else if (th->ack && th->fin ) { + // If both ACK and FIN are set, then it is graceful termination from server. + *flags |= FIN_ACK_FLAG; + } else if (th->ack && th->rst ) { + // If both ACK and RST are set, then it is abrupt connection termination. + *flags |= RST_ACK_FLAG; + } else if (th->fin) { + *flags |= FIN_FLAG; + } else if (th->syn) { + *flags |= SYN_FLAG; + } else if (th->ack) { + *flags |= ACK_FLAG; + } else if (th->rst) { + *flags |= RST_FLAG; + } else if (th->psh) { + *flags |= PSH_FLAG; + } else if (th->urg) { + *flags |= URG_FLAG; + } else if (th->ece) { + *flags |= ECE_FLAG; + } else if (th->cwr) { + *flags |= CWR_FLAG; + } +} + +// Extract L4 info for the supported protocols +static inline void fill_l4info(void *l4_hdr_start, void *data_end, u8 protocol, + struct l4_info_t *l4_info) { + switch (protocol) { + case IPPROTO_TCP: { + struct tcphdr *tcp = l4_hdr_start; + if ((void *)tcp + sizeof(*tcp) <= data_end) { + l4_info->src_port = bpf_ntohs(tcp->source); + l4_info->dst_port = bpf_ntohs(tcp->dest); + set_flags(tcp, &l4_info->flags); + } + } break; + case IPPROTO_UDP: { + struct udphdr *udp = l4_hdr_start; + if ((void *)udp + sizeof(*udp) <= data_end) { + l4_info->src_port = bpf_ntohs(udp->source); + l4_info->dst_port = bpf_ntohs(udp->dest); + } + } break; + case IPPROTO_SCTP: { + struct sctphdr *sctph = l4_hdr_start; + if ((void *)sctph + sizeof(*sctph) <= data_end) { + l4_info->src_port = bpf_ntohs(sctph->source); + l4_info->dst_port = bpf_ntohs(sctph->dest); + } + } break; + case IPPROTO_ICMP: { + struct icmphdr *icmph = l4_hdr_start; + if ((void *)icmph + sizeof(*icmph) <= data_end) { + l4_info->icmp_type = icmph->type; + l4_info->icmp_code = icmph->code; + } + } break; + case IPPROTO_ICMPV6: { + struct icmp6hdr *icmp6h = l4_hdr_start; + if ((void *)icmp6h + sizeof(*icmp6h) <= data_end) { + l4_info->icmp_type = icmp6h->icmp6_type; + l4_info->icmp_code = icmp6h->icmp6_code; + } + } break; + default: + break; + } +} + +// sets flow fields from IPv4 header information +static inline int fill_iphdr(struct iphdr *ip, void *data_end, flow_id *id, u16 *flags) { + struct l4_info_t l4_info; + void *l4_hdr_start; + + l4_hdr_start = (void *)ip + sizeof(*ip); + if (l4_hdr_start > data_end) { + return DISCARD; + } + __builtin_memset(&l4_info, 0, sizeof(l4_info)); + __builtin_memcpy(id->src_ip, ip4in6, sizeof(ip4in6)); + __builtin_memcpy(id->dst_ip, ip4in6, sizeof(ip4in6)); + __builtin_memcpy(id->src_ip + sizeof(ip4in6), &ip->saddr, sizeof(ip->saddr)); + __builtin_memcpy(id->dst_ip + sizeof(ip4in6), &ip->daddr, sizeof(ip->daddr)); + id->transport_protocol = ip->protocol; + fill_l4info(l4_hdr_start, data_end, ip->protocol, &l4_info); + id->src_port = l4_info.src_port; + id->dst_port = l4_info.dst_port; + id->icmp_type = l4_info.icmp_type; + id->icmp_code = l4_info.icmp_code; + *flags = l4_info.flags; + + return SUBMIT; +} + +// sets flow fields from IPv6 header information +static inline int fill_ip6hdr(struct ipv6hdr *ip, void *data_end, flow_id *id, u16 *flags) { + struct l4_info_t l4_info; + void *l4_hdr_start; + + l4_hdr_start = (void *)ip + sizeof(*ip); + if (l4_hdr_start > data_end) { + return DISCARD; + } + __builtin_memset(&l4_info, 0, sizeof(l4_info)); + __builtin_memcpy(id->src_ip, ip->saddr.in6_u.u6_addr8, IP_MAX_LEN); + __builtin_memcpy(id->dst_ip, ip->daddr.in6_u.u6_addr8, IP_MAX_LEN); + id->transport_protocol = ip->nexthdr; + fill_l4info(l4_hdr_start, data_end, ip->nexthdr, &l4_info); + id->src_port = l4_info.src_port; + id->dst_port = l4_info.dst_port; + id->icmp_type = l4_info.icmp_type; + id->icmp_code = l4_info.icmp_code; + *flags = l4_info.flags; + + return SUBMIT; +} + +// sets flow fields from Ethernet header information +static inline int fill_ethhdr(struct ethhdr *eth, void *data_end, flow_id *id, u16 *flags) { + if ((void *)eth + sizeof(*eth) > data_end) { + return DISCARD; + } + __builtin_memcpy(id->dst_mac, eth->h_dest, ETH_ALEN); + __builtin_memcpy(id->src_mac, eth->h_source, ETH_ALEN); + id->eth_protocol = bpf_ntohs(eth->h_proto); + + if (id->eth_protocol == ETH_P_IP) { + struct iphdr *ip = (void *)eth + sizeof(*eth); + return fill_iphdr(ip, data_end, id, flags); + } else if (id->eth_protocol == ETH_P_IPV6) { + struct ipv6hdr *ip6 = (void *)eth + sizeof(*eth); + return fill_ip6hdr(ip6, data_end, id, flags); + } else { + // TODO : Need to implement other specific ethertypes if needed + // For now other parts of flow id remain zero + __builtin_memset(&(id->src_ip), 0, sizeof(struct in6_addr)); + __builtin_memset(&(id->dst_ip), 0, sizeof(struct in6_addr)); + id->transport_protocol = 0; + id->src_port = 0; + id->dst_port = 0; + } + return SUBMIT; +} + +static inline void set_key_with_l2_info(struct sk_buff *skb, flow_id *id, u16 *family) { + struct ethhdr eth; + __builtin_memset(ð, 0, sizeof(eth)); + bpf_probe_read(ð, sizeof(eth), (struct ethhdr *)(skb->head + skb->mac_header)); + id->eth_protocol = bpf_ntohs(eth.h_proto); + __builtin_memcpy(id->dst_mac, eth.h_dest, ETH_ALEN); + __builtin_memcpy(id->src_mac, eth.h_source, ETH_ALEN); + if (id->eth_protocol == ETH_P_IP) { + *family = AF_INET; + } else if (id->eth_protocol == ETH_P_IPV6) { + *family = AF_INET6; + } + } + +static inline void set_key_with_l3_info(struct sk_buff *skb, u16 family, flow_id *id, u8 *protocol) { + if (family == AF_INET) { + struct iphdr ip; + __builtin_memset(&ip, 0, sizeof(ip)); + bpf_probe_read(&ip, sizeof(ip), (struct iphdr *)(skb->head + skb->network_header)); + __builtin_memcpy(id->src_ip, ip4in6, sizeof(ip4in6)); + __builtin_memcpy(id->dst_ip, ip4in6, sizeof(ip4in6)); + __builtin_memcpy(id->src_ip + sizeof(ip4in6), &ip.saddr, sizeof(ip.saddr)); + __builtin_memcpy(id->dst_ip + sizeof(ip4in6), &ip.daddr, sizeof(ip.daddr)); + *protocol = ip.protocol; + } else if (family == AF_INET6) { + struct ipv6hdr ip; + __builtin_memset(&ip, 0, sizeof(ip)); + bpf_probe_read(&ip, sizeof(ip), (struct ipv6hdr *)(skb->head + skb->network_header)); + __builtin_memcpy(id->src_ip, ip.saddr.in6_u.u6_addr8, IP_MAX_LEN); + __builtin_memcpy(id->dst_ip, ip.daddr.in6_u.u6_addr8, IP_MAX_LEN); + *protocol = ip.nexthdr; + } + } + +static inline int set_key_with_tcp_info(struct sk_buff *skb, flow_id *id, u8 protocol, u16 *flags) { + u16 sport = 0,dport = 0; + struct tcphdr tcp; + + __builtin_memset(&tcp, 0, sizeof(tcp)); + bpf_probe_read(&tcp, sizeof(tcp), (struct tcphdr *)(skb->head + skb->transport_header)); + sport = bpf_ntohs(tcp.source); + dport = bpf_ntohs(tcp.dest); + set_flags(&tcp, flags); + id->src_port = sport; + id->dst_port = dport; + id->transport_protocol = protocol; + return tcp.doff * sizeof(u32); + } + +static inline int set_key_with_udp_info(struct sk_buff *skb, flow_id *id, u8 protocol) { + u16 sport = 0,dport = 0; + struct udphdr udp; + + __builtin_memset(&udp, 0, sizeof(udp)); + bpf_probe_read(&udp, sizeof(udp), (struct udp *)(skb->head + skb->transport_header)); + sport = bpf_ntohs(udp.source); + dport = bpf_ntohs(udp.dest); + id->src_port = sport; + id->dst_port = dport; + id->transport_protocol = protocol; + return bpf_ntohs(udp.len); + } + +static inline long tcp_drop_lookup_and_update_flow(struct sk_buff *skb, flow_id *id, u8 state, u16 flags, + enum skb_drop_reason reason) { + flow_metrics *aggregate_flow = bpf_map_lookup_elem(&aggregated_flows, id); + if (aggregate_flow != NULL) { + aggregate_flow->tcp_drops.packets += 1; + aggregate_flow->tcp_drops.bytes += skb->len; + aggregate_flow->tcp_drops.latest_state = state; + aggregate_flow->tcp_drops.latest_flags = flags; + aggregate_flow->tcp_drops.latest_drop_cause = reason; + long ret = bpf_map_update_elem(&aggregated_flows, id, aggregate_flow, BPF_ANY); + if (trace_messages && ret != 0) { + bpf_printk("error tcp drop updating flow %d\n", ret); + } + return 0; + } + return -1; + } + +#endif // __UTILS_H__ diff --git a/e2e/cluster/base/04-agent.yml b/e2e/cluster/base/04-agent.yml index 19a0ff521..7771b9f5d 100644 --- a/e2e/cluster/base/04-agent.yml +++ b/e2e/cluster/base/04-agent.yml @@ -32,3 +32,12 @@ spec: fieldPath: status.hostIP - name: FLOWS_TARGET_PORT value: "9999" + volumeMounts: + - name: bpf-kernel-debug + mountPath: /sys/kernel/debug + mountPropagation: Bidirectional + volumes: + - name: bpf-kernel-debug + hostPath: + path: /sys/kernel/debug + type: Directory diff --git a/e2e/ipfix/manifests/30-agent.yml b/e2e/ipfix/manifests/30-agent.yml index d6e50d848..8a0b09fc3 100644 --- a/e2e/ipfix/manifests/30-agent.yml +++ b/e2e/ipfix/manifests/30-agent.yml @@ -34,3 +34,12 @@ spec: fieldPath: status.hostIP - name: FLOWS_TARGET_PORT value: "9999" + volumeMounts: + - name: bpf-kernel-debug + mountPath: /sys/kernel/debug + mountPropagation: Bidirectional + volumes: + - name: bpf-kernel-debug + hostPath: + path: /sys/kernel/debug + type: Directory diff --git a/e2e/kafka/manifests/30-agent.yml b/e2e/kafka/manifests/30-agent.yml index e8ac40835..6602d8574 100644 --- a/e2e/kafka/manifests/30-agent.yml +++ b/e2e/kafka/manifests/30-agent.yml @@ -30,3 +30,12 @@ spec: value: 200ms - name: LOG_LEVEL value: debug + volumeMounts: + - name: bpf-kernel-debug + mountPath: /sys/kernel/debug + mountPropagation: Bidirectional + volumes: + - name: bpf-kernel-debug + hostPath: + path: /sys/kernel/debug + type: Directory diff --git a/examples/flowlogs-dump/server/flowlogs-dump-collector.go b/examples/flowlogs-dump/server/flowlogs-dump-collector.go index 2728e0b3b..d04af1c02 100644 --- a/examples/flowlogs-dump/server/flowlogs-dump-collector.go +++ b/examples/flowlogs-dump/server/flowlogs-dump-collector.go @@ -72,7 +72,7 @@ func main() { for records := range receivedRecords { for _, record := range records.Entries { if record.EthProtocol == ipv6 { - log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v\n", + log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v dnsId: %d dnsFlags: 0x%04x dnsReq: %v dnsRsp: %v\n", ipProto[record.EthProtocol], record.TimeFlowStart.AsTime().Local().Format("15:04:05.000000"), record.Interface, @@ -81,16 +81,20 @@ func main() { net.IP(record.Network.GetDstAddr().GetIpv6()).To16(), record.Transport.DstPort, protocolByNumber[record.Transport.Protocol], - record.Icmp.IcmpType, - record.Icmp.IcmpCode, + record.IcmpType, + record.IcmpCode, record.Direction, record.Bytes, record.Packets, record.Flags, record.TimeFlowEnd.AsTime().Local().Format("15:04:05.000000"), + record.GetDnsId(), + record.GetDnsFlags(), + record.GetTimeDnsReq(), + record.GetTimeDnsRsp(), ) } else { - log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v\n", + log.Printf("%s: %v %s IP %s:%d > %s:%d: protocol:%s type: %d code: %d dir:%d bytes:%d packets:%d flags:%d ends: %v dnsId: %d dnsFlags: 0x%04x dnsReq: %v dnsRsp: %v\n", ipProto[record.EthProtocol], record.TimeFlowStart.AsTime().Local().Format("15:04:05.000000"), record.Interface, @@ -99,13 +103,17 @@ func main() { ipIntToNetIP(record.Network.GetDstAddr().GetIpv4()).String(), record.Transport.DstPort, protocolByNumber[record.Transport.Protocol], - record.Icmp.IcmpType, - record.Icmp.IcmpCode, + record.IcmpType, + record.IcmpCode, record.Direction, record.Bytes, record.Packets, record.Flags, record.TimeFlowEnd.AsTime().Local().Format("15:04:05.000000"), + record.GetDnsId(), + record.GetDnsFlags(), + record.GetTimeDnsReq(), + record.GetTimeDnsRsp(), ) } } diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index efd44c821..2553feef6 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -122,7 +122,7 @@ func FlowsAgent(cfg *Config) (*Flows, error) { debug = true } - fetcher, err := ebpf.NewFlowFetcher(debug, cfg.Sampling, cfg.CacheMaxFlows, ingress, egress) + fetcher, err := ebpf.NewFlowFetcher(debug, cfg.Sampling, cfg.CacheMaxFlows, ingress, egress, cfg.EnableTCPDrops, cfg.EnableDNSTracking) if err != nil { return nil, err } diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 6dfb49b2d..7cc146295 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -138,4 +138,8 @@ type Config struct { ProfilePort int `env:"PROFILE_PORT"` // EnableGC enables golang garbage collection run at the end of every map eviction, default is true EnableGC bool `env:"ENABLE_GARBAGE_COLLECTION" envDefault:"true"` + // EnableTcpDrops enable TCP drops eBPF hook to account for tcp dropped flows + EnableTCPDrops bool `env:"ENABLE_TCP_DROPS" envDefault:"false"` + // EnableDNSTracking enable DNS tracking eBPF hook to track dns query/response flows + EnableDNSTracking bool `env:"ENABLE_DNS_TRACKING" envDefault:"false"` } diff --git a/pkg/ebpf/bpf_bpfeb.go b/pkg/ebpf/bpf_bpfeb.go index 575ad0681..c1037c2d1 100644 --- a/pkg/ebpf/bpf_bpfeb.go +++ b/pkg/ebpf/bpf_bpfeb.go @@ -13,6 +13,13 @@ import ( "github.com/cilium/ebpf" ) +type BpfDnsRecordT struct { + Id uint16 + Flags uint16 + ReqMonoTimeTs uint64 + RspMonoTimeTs uint64 +} + type BpfFlowId BpfFlowIdT type BpfFlowIdT struct { @@ -39,6 +46,8 @@ type BpfFlowMetricsT struct { EndMonoTimeTs uint64 Flags uint16 Errno uint8 + TcpDrops BpfTcpDropsT + DnsRecord BpfDnsRecordT } type BpfFlowRecordT struct { @@ -46,6 +55,14 @@ type BpfFlowRecordT struct { Metrics BpfFlowMetrics } +type BpfTcpDropsT struct { + Packets uint32 + Bytes uint64 + LatestFlags uint16 + LatestState uint8 + LatestDropCause uint32 +} + // LoadBpf returns the embedded CollectionSpec for Bpf. func LoadBpf() (*ebpf.CollectionSpec, error) { reader := bytes.NewReader(_BpfBytes) @@ -89,6 +106,8 @@ type BpfSpecs struct { type BpfProgramSpecs struct { EgressFlowParse *ebpf.ProgramSpec `ebpf:"egress_flow_parse"` IngressFlowParse *ebpf.ProgramSpec `ebpf:"ingress_flow_parse"` + KfreeSkb *ebpf.ProgramSpec `ebpf:"kfree_skb"` + TraceNetPackets *ebpf.ProgramSpec `ebpf:"trace_net_packets"` } // BpfMapSpecs contains maps before they are loaded into the kernel. @@ -135,12 +154,16 @@ func (m *BpfMaps) Close() error { type BpfPrograms struct { EgressFlowParse *ebpf.Program `ebpf:"egress_flow_parse"` IngressFlowParse *ebpf.Program `ebpf:"ingress_flow_parse"` + KfreeSkb *ebpf.Program `ebpf:"kfree_skb"` + TraceNetPackets *ebpf.Program `ebpf:"trace_net_packets"` } func (p *BpfPrograms) Close() error { return _BpfClose( p.EgressFlowParse, p.IngressFlowParse, + p.KfreeSkb, + p.TraceNetPackets, ) } diff --git a/pkg/ebpf/bpf_bpfeb.o b/pkg/ebpf/bpf_bpfeb.o index f3dd6b1ea09dea6751857a74838cac9ded0401a6..cdaf6f13945bac619aaad7889be15ae739665777 100644 GIT binary patch literal 67240 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 aEkWXF_%bNpgOW8U9f9<~_I1GgF987UZa`ZA literal 27208 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 diff --git a/pkg/ebpf/bpf_bpfel.go b/pkg/ebpf/bpf_bpfel.go index bae6823f0..4aa8bfd8c 100644 --- a/pkg/ebpf/bpf_bpfel.go +++ b/pkg/ebpf/bpf_bpfel.go @@ -13,6 +13,13 @@ import ( "github.com/cilium/ebpf" ) +type BpfDnsRecordT struct { + Id uint16 + Flags uint16 + ReqMonoTimeTs uint64 + RspMonoTimeTs uint64 +} + type BpfFlowId BpfFlowIdT type BpfFlowIdT struct { @@ -39,6 +46,8 @@ type BpfFlowMetricsT struct { EndMonoTimeTs uint64 Flags uint16 Errno uint8 + TcpDrops BpfTcpDropsT + DnsRecord BpfDnsRecordT } type BpfFlowRecordT struct { @@ -46,6 +55,14 @@ type BpfFlowRecordT struct { Metrics BpfFlowMetrics } +type BpfTcpDropsT struct { + Packets uint32 + Bytes uint64 + LatestFlags uint16 + LatestState uint8 + LatestDropCause uint32 +} + // LoadBpf returns the embedded CollectionSpec for Bpf. func LoadBpf() (*ebpf.CollectionSpec, error) { reader := bytes.NewReader(_BpfBytes) @@ -89,6 +106,8 @@ type BpfSpecs struct { type BpfProgramSpecs struct { EgressFlowParse *ebpf.ProgramSpec `ebpf:"egress_flow_parse"` IngressFlowParse *ebpf.ProgramSpec `ebpf:"ingress_flow_parse"` + KfreeSkb *ebpf.ProgramSpec `ebpf:"kfree_skb"` + TraceNetPackets *ebpf.ProgramSpec `ebpf:"trace_net_packets"` } // BpfMapSpecs contains maps before they are loaded into the kernel. @@ -135,12 +154,16 @@ func (m *BpfMaps) Close() error { type BpfPrograms struct { EgressFlowParse *ebpf.Program `ebpf:"egress_flow_parse"` IngressFlowParse *ebpf.Program `ebpf:"ingress_flow_parse"` + KfreeSkb *ebpf.Program `ebpf:"kfree_skb"` + TraceNetPackets *ebpf.Program `ebpf:"trace_net_packets"` } func (p *BpfPrograms) Close() error { return _BpfClose( p.EgressFlowParse, p.IngressFlowParse, + p.KfreeSkb, + p.TraceNetPackets, ) } diff --git a/pkg/ebpf/bpf_bpfel.o b/pkg/ebpf/bpf_bpfel.o index b6095610414e142b585363b87c85ed31fa4fe8f5..df2199eef85efc66c71fe22bac3642b760bfa572 100644 GIT binary patch literal 67888 zcmb<-^>JfjWMqH=MuzVU2p&w7fnftPLev2)?7$$#z{s#)1I%IAFAkw4*dR2FOiYBZ zL>W23j8+B)26hGp2AFs{0|SEs0|P@jl-|z-R>oj21flhzv^+==0|P@SgAp@Grn)QR zKZs7O3;<CK)l~%$euWR1za8q_Y^fh$@oK3LQ2GUwegLI!K<Num`UI3d0H#6iWo2Ms z*xv{?mVup-fq{>KfkAhoBG?_<q3+$^43XzohR}sZAYBX$#aidU<`f#SfF+8x&O-TM z?F_|QXP|tDLB(39p?ru3inUHb_)TE#48>X}!F-UtprFRI7iP{;u)0D>a4{5X9fR<j zK)OKgIt<|(!R$E#<-^>09Lk6J0}?KoO*~+S?1%dE4l@IT00RR<wiP72M45aU+U_zi zgfeYn;9y{2*v`fPPEXrGkq(M`4h9A#Q25Po1DgZ#2gssgty>_9p;+rClzs)JUxI0n zIUo(iS=Ye);;gGs`U;f345cqY>5EVr60gNs=fV8ytO_t)oCS&Z;;idX{xc~36iPpV z(vP9^BPjh4N<-35an^k(ACj($vz~+bi9)F1RL;!6Ai==EP;7M%EDsAmkT@v(immQ~ z#f!CWgXwZ625|TlYuy3!6NNyk7#NWCLCkvxmPZx`sVug73l=ZddJU!#=Dh*)3xy#0 zu~_Rfgf9%??_UB6Wd;Uz76yij|NsBP(*6EP5b^!ccx)A5VEDttz_7mnB3`(#25e5T z)E6*axUd#X7fXEw)0qpQ;V$(a%r9qz`VX2fGZ$8Y#fzmrf@y^OCon&Ac^#MzW!nT! z;H(TiV2eTN2b6-5)0Y`IeI>x<L5ylGi2d7{A@=TvmMi>5AXx^6LS=q10~SAp%Ai~h ziZ4jK6e{yV<stD=s0@kUVk<~|6)JN><ss>~P?-zLhot91Wk~oITS3xYp)x3UfXs)a zzd~h5z!qCU(s!XUG@n{Q(s`jWG#^?)(tDvYG@n{Q(tV*av^=qbr2j%?a4<0xTS4+c zp%N&RK=~e$9}0z__G>}%1t?rV=?ODjVD5nA3xqo%`32z)NIpTh6Ow-r?ttVQggYVm zsZbbXD<~Ww`KVAC>P{<2{zAk9Bt0YI36ky+@c>EZh<Jjgb9g*J$`M37LCP0IJV5eo zp%OHnv>^GpPzdUOcs_yf3FZ@sG6qmSna;$(07|Ez1hF5IEE(vQUzmKr34xVC4P-q7 zET0rtLGo2`6(qkES3&B>;wnh~EUtotZ*dhQ+>5Iq;aOY-3FqP}Nca_3LBh4T3KCw$ zRgiEju7aeG;wnhGDXxN~hvF(oIw`J##D8%WBwZ9&LE^i(3X%?rt03iMaTO&06<0y> zU2zp8zZF+O@>y{eB!3lGLGo2`6(m0uS3&YoaTO&06jwp=O>q?@zZ6$N@=0+OB!3iF zLGnd$6(m0tS3%0f{gC9qP`LuyzG#Frcq$h`+8@PQhrs1hw$wK;4Jx-l<u9z=v40Ub z*csTF7#I%x|NmbClDr{;3=9mQ_60~tl%WVLRtRm2r9;cdLU8OcF!(Eir5OsDAo)BU zT0ayrLlpQ!%Q28R$bNrl`Bumb@jyDX+$&^+<a>W%sCkT#atm%B3#8ogcZABbLdu<V zR!F&7$O0*M{9U2)ERb?5odqh-21)q-DG+&(IM_WQP(CB1z2I*M;e)~j<bRC%%K+jp zWN}bQQEUM5e=)Q@2(Q-^K&CJ-fcycHg1etd7HknHTtGfAWR?N*Vd3Qu33i4=So;m? zuKiQM${6-LLugPvZ2_vk4Wz*W$l?ZIaVZROP)aE_ki-xNxv$s&<QoPQ^FboT29Sm{ zviYF!D>e|rFbCwHVgpgIc(H>BgjRs~4-qa9|HI04kT}@A3Xph476<!J0g@h&#lh}X z0NKXCfGiI7p8_O(A&Z0Es{ly{$l_rCDL~Rou>d3;f$|H;<YECxIwO={_CundVLL=S zIRAiB3VOaO6a)tcgDB$~aKaE}SPiB@>cSyXU=ma=gWLr(7nC1CE@x#}3RVy5M}Qc~ zQjqpTBD5c{e==A*!+vPK1BELnoRYOBfXjhotqurn)dQtlAhZ;uos?ZQ1<VJT4>BoR zY7Us6Ej0^D&w$d?z;v=y1DMX1S^}jPLFolhdLEQs1Ep6%=@n3V8I;}vr8hz84N!U= zl->iScR}eLP<k7fMvtHUi$ShuU|?s4v=>3)3lV{k=<&246u1lw45Dn6pkQS{SC5D{ zNO}Ot=R=GFlc4wl<%IpoU@kcRK}NQN2v9!Ef`k`HA4CO&1nFx4^+%xT2NchsEFa1M zNx$go5$;HVyAz}bVj84AH-(npAgfs!;=$@N7ee}*#R8z*#K3?^-<Dvll?x&DEONd8 zsVr772g@UiL-MH^hBzeOfow<3kD&BdtYCtn9+bX|6^t>&A^8-NfQtn{wjsM0QV>=y zgw*TE?t-{iAHzJ53B?L}7~&B3>SBmP+^d5j4sowGhB(B%T43>F0g$Q4?$rc~S1yFK zBQV{o4wgq2hqxEgE<zRunOv-(ilH9jUKI>+h<hO&Vq|k5?gjOB85oKMK)R9L3#rg6 z7ed-C$oe4RE019h#JzGD;t=;@mh&JRi=p)aviT7ALhCVTI{{RFLEH<e$BG3&s*v3a zsmCf8LfT2l=0V&Gt+$ZOgSZ!zTT#;~#J!Mu3)viqdm;50vN*^k#R`xH3$i%Gy$}nF z1wcGx_d+Uw%7u_|3uJu|_d@!6$l?(9Li&Bk;t=;j`hUpc5cfj*fym+z_hQzgAQu)x z>)m1jP_2jTUPw7qxezjrf@~hdy^wknSsdbCQ0ap$d?Dp0vU-SnAr&XGIK;h>f)QC9 z;$Beg3QE7=WQSCqfr~qaN@%|xSs%o`kOm~OIK;h>(L-c$Nch@=EkG8BxYrIt9O7PE z3~`8iZNTEi0#;xeQT|$k`IQSH^(eAE5chh5<&ni9?)AVBhq%`rLmc8>$ha1=IS}_k z8qCPz5cj%(%_$ae0@DchLK>cx3nA?YWPK3#27=Wii$mNC84pDkhq%`tLp{X3ei-5q z_xfUpL);4)Uo95!0?Q-Z>kZ~tE`+pyknM-KHxew5EDmvR1co@oy^vNKvU-Sn!!XoC z+#8A^4skD}<54US1eQm*HyF&XTnK4rA=?9SFJ#~vSsdbCNCzHS9OB-1uzASh5ckGm zh(p{Piy;niZwy$xSRe{aBitJe=2tF+wD*whfw(slERQS>ac>5OIK;i6k$2Q`1LEE^ z4D}HArecUg+zaXG6$>PR<q__MG|(y+LfW6m?t-|t5Ud_q9O7QcfFiOu#J!MKAhI~b zy?GesK-`;)Ar5hG4p_WcAPY<*+?x&NS1yFKhmq}pxVI84k1P&xZv}=p#J!*y4Q%0C zhM^wf-ck&4h<i)G;>7}xjw-^v#bEKog^>0+vb!MW)q>@b#UbX^fW?ais=zeDylOB% zb0MVtUo21u<|E|m!TkM@Vjn)91!@zwFfcGo1<NupWC}y-!*Eb_BEY~P$_%M*K;w&` ziGWNIs60rRiGd-M1=5bhtB;9+0Yx7pNFOMa3E0PirjG@r57O=exd-GGOmiXaD3Cm; z=0lMOyBE?90?C8I6GdJCWIm)_1Cj@M5k($sKcrm(k_WW_P~<s4`XTKMkUS`TqsS|O z<RR?@kUXd+MUi&^$wTUWkUXfrlPL%ZZ_v01D7_wMWMBZ%sf^4F|NsC0p9u*L2H1E^ zHl#TPkIz(Q0r32eD6=#u`WQr+85uw=4ps(8|6o6)SqAR^fm&|5%8>SdCe)leOpx)O zY)G@2AyWtxstgPWaR?tY-vBaqKcpH2kDG4$|NlQL14N@Jvk(I)<UsBK)$Aa5fYKc+ zc=nKi0offOAL}YZ;t{7i1R?gaf@kwk^@HL?R~ZtIg;0MKtAJvYfuT?klxi3lSizx! zO&_HFNXXth&~dPA5tuum?K`CKgt!B0FAjGUi$LNVmj7V$u(0{E>MY1OWp!2pxc^?A z6#=CKptJ{+c7W0rP}%@WYd~oQC@le{1)wwslxBd^KmLJ=g6gafQ2GUwegLI!K<Num z`UI3d0Ht?8=?zeN1(aR@rDs6t2~fHNOjj0zLW_Z6|3Z-M4B&a%j(`9EgZlk1p#5}E zNG4W5#>YV8E})RtZM*@FP;`Bj(0K__<||-%ka|!Q@I&JVHjbWM1)1;4hK{FaL&wpx zq2uS-&~fu@=y-WHbeudJIzFBa9T(4rj)!MM$HB9q<KNlPaqn#Ccy~5*oI4vjzMTyn z*UpBHXJ<plv9qfn^Um2-koo28D#$!?b`_*QlwAdx56-TF%=>0nLB_F*tM-HAF}n&< z4<Luj{{N8h1O*7F+y{jxvOFlf6NM&#BOqG~Ivx(o|5}i7k?bnS_*ZfjWIPQvZ=Wf& z3#_kF_ym}S&7<#M2IexbvoSCffaW71%`$NP1IlSIcYxvtq;fx`8N$F1iYx{OhC;|R zJwve+BpeDAL9x!jP%H(VXIBLI2E>PiPa)L&VkyWxd!ZsEe-%qX!n05j<ZF<6$UHvC zUU0dE(_Tn8BkYCF%fs!3gnwZo$P|#d(0O^dy^#1q*bAAL&rAfBatsXcazqN!Zxm&O z4D^dK+A-X1gUr8!!iANg8thT@_{tR80g`4w5BKCMNI$6>EuQej2dMl<jt@{mDwcxI z(?~(*X{4a@MpCeOBTDCKAmb;9c^^vWogm{~uz9RRXgDLcqrmMeEy(;7vN)(cP^<-= zuabhyQ^DI$Qjqzp#6*xP)OI1nJji?-rg_l$Hz~-x8^S!ud|Y87q&>jO0O>D*(jzEB ztEC|Quxcqt|FIf6eq0S5$F7!w%x4js9`MCGs3iuEcj&y-erP`8hqf;Yg&_4?u@)r0 z5%a2$`ME+NXg<?|%+nPLLGzgwWWKIY2-Khi&0j<2?Fymoh+-|s{9U0CIC(P^YeB}R z3X>r9K{0fGyHE%+-ct;Xw?ZLM=>aki5?_Tvkaibry+t8({RV73MxhX7JO{Q;rBDbm ze*#;tf>=)hNsonsQ2V7I^O2x>2o%q_!v&IV5#a)vH${XCWd0NpE|7UtM7TiaQxV|; znO8-G3uIihFbU)ekiQ}GtcY-dq(4NsK++u|T%hR<9xjmdhzJ)*zD9%#B!45q1(L52 z;R2Z_hLwA;_DVP-1Gpg(%J>xA&aCcg0N3xB>2w<?nPZe&FmsZ#pyd~Iy-#%(G`%ke zYhz$%ht?;MZYa1uK`y^CA>BQO%nC?-fjsZA2UHJ1^?~O19hnj9!$9RRs9g@qr{I30 z6l9+L4hN(iT-*hjZ{I%=;tovpkokc-oKW?%AoKB{d=080ilrd)(#2Vj`nqsAq`uBv z39YY*oNo?gfQ*-b%mw9~Vkt_^gN*-x$^lSLMh-7fy8vVl$j2~yVDoLDc~ww9C)o;8 zP9<AG;w_mAI$y>GX)k0hhWZaOy}?ipoo_Dog3Oy$E`_EO$Z!u>9#qn1dqKuavZEmL zW8nEHMh1p#uRc)q#87NC8%$?I=jjR;L&CGzYA#qjQ3*O80U6$B0J#^GgNm&n^GwL| z(;)kbt)_zY6>Cif)1Yy6P)Sg%1)1-I%_9~|O#;goOHG8*kn*}1I{#NJH4`ch8J{Ya znhxe?OF_m{vZY}2dFb<jknxLbDcF1-`g|bj{2#m?Kub5s^Ln6m7yA4dI2@tvYETY= z&Fi77L(HQ=##chwK&cJXe}arxfz*Tgb)d8X!k}^zH1h(R-(zKfj3X5?fl>(r1AHEz z5i&mlU+2IGnP&m@b3x%<$PB4?VEtN<I4FML>nNBZ{Yv<{2qs7b`$H&(LMF((5Zpc% zkgFIN{Gt6*&^l0%PxgZf4bb{k7HEETfY^&H4o)u)5cd@eK->)~A3*Me%?p9-0lN<} z9mi0}460Qb7~t+>gv?(d)@?xM!9e~3nN!FDng4>VdjN@p&4YB)5$hr#&SOB&pRjZe ztFJ2;L-P~VJ)m?9_J;z*K;(IEkUhl;piu<|l=*OwyNVSc<2%I$kZ}RnIzo_n5P8V_ zU-1OUd|z<^<~$+D<YEPg|B%~9;PO=g5)R1Xpp;Uq00}Q-aZrdBD}YGU_7}wckoZMb z5AiP~9TW>d5(XmPAn6A=9I>tMft7RU`302jLFpZa5%~mE$}=#CGJr}^2JrkiWCj?b z5|lqcQ3x{^RNjJgz}A()#sOgc)9NZ{zrPCF@2`UP`>UY+{wip{zY5y#uY&gbtDyb< zDrmpI3fk|lg7*8Xp#A<TaCOa4T?Os;S3&#zRnUHa6|~=91?~4&LHqqx(0+dvwBKI^ z?e|wf`~6kWet#9T-(Lmo_g6vt{Z){De{~fkomL8iQWFCMayzx*-~a!x_7*7IP{sig zq5V{7ISGnaP&&w#f|j4q{!eyQ9i-fa_FJ>7AnR1Kt047Nc2yfx9#UUrS2aQTkn%jc z3R2HxS3%Z+WLI@T<ss#Ib`_)^fvpG0u7cDH*;UYbs|r$YWmiG!x$G+F`j9GUJx~R$ z2dbd;KoxXdNENi)uY#@vsValo3+ZnrS3&xV)m4!3<IF^8|2rAl|AvlpAcyA;P`d}( zt_OuDs09s5f1s8+tbWJc|AvGQq95E0c26O^9}EdMME|=HDi6sA#Zr)PM)ZSQ!SWz` z!Ra4o{~Hp22zw#xk`eYo+83~PXt7i^*jz-vAJQH{*jo>l$LxPY#xq12Aq^8zMtgAo z8&rmX##te`7gX<=fT|6YaVk(c#oZ5w^#7~T;tOBAfT|?qcmX*Wv%LXHC&f|=LDCGx z&~#c{)eGfA(ra;5H<S-ax5ZVRP(CF67gx1I`H=KpT-6WdL(*+=6{H<i3{AhqRgm^p zF*F?)S3%o3RgnBpTm@<86hqU0an*dVdAR%Gko<#OZiDi1vDQ?uzG5k8I}Ey>vRDez z4#V9KhvX+r^Oi!*TMVW_<prq3DTc1c!rc#t^arb@ApO#6DQJ8`$7`ykAo&lue}gYQ z;EQ)q%?6Kmg8gtvxrFF%LgF3K--N_JqQ41A2Z;VABt0Pdn~-#Y=x;*O2cjPiNhgT@ zCM4bv{Y^-GA^Mw;ctZ5UA@PXlZ$ipRM1K>~jz;u1A?Xp(--M(~P`L%lldyUL*4{yk zPe9TwB3vNp7ZEOyau^XVko1fQ7fAVx2p35DMuZC_og@0;ko1lS7fAX;gbO6yA;JYx z&LhGFk{%J^0?F5iaDn7+M7Tie6-2l|@)xZ9gY~~f88yKDZ%|kvmtUcbbHMX3pmG;f zrr}P%*!$=Ek^0}D`UKQp2j!#v(Ec<ien5O!I!3o2WG|?{P%O0?oDbpsbLhHE%>Fc_ z{)6@Bare6+^(msCz7lLcq948-%+D+Y)pm$+S5QCT`(Mbo`2WBE|LY1t=7U)oAni$z zdqLrpOmx1-5pGE1;Gl4WrC(5ZA(!`=kZu)2W(KHN#(>h!FGH%o36EPq!i`XPf&2%m ze_`=KF#OUW<JRSD3=AL_hO;p+fL82m2L%#nT_^`5188MqD5pP|Mour_a<LE^KE<f* z6n;oI5v5+ufX-KdY=ot!%#~T-cns%cU;xdJ!u$s+&p{)U%NZg2)rzGc?YzV?NdG@` z1*H9i+};Q6k3w(9gX{rS@bG<744^unfgzL~GH?M3FHn14loO<ifdRw^t#AUxBS<7! z3sN6|_@E$3hK`43cR|KQlcD=kG8aSU^$`2-AmeYzT99$N%*7CYBIF_B>1>ewmdBxV zau#G<1r#5kep7N5q+OV-1*x}^wIJh;$y$(cscfx3;P6X^&gUd+LFT`bwIJh*$<Y03 z$y|{6mSiqSdlR;=6V&em+XEe^P1b_chsfg%;Ped%E(U&3;s)(6gwDUh)*pc43)C;n z76GL`28L`%wgm4Z1=auAETGhdn*OoH7ka-8mJZ?lD2OrO`Djo%h)o_8Z=n2yO&%17 zNcjK%)S{yNB88IV0)>>K`~rpKqSVBa%)E4kywq}qw4D5M1=SQT1~f&b1t}0kXbNzN zC#Ix;jD)LB&M(bL$x|;;C`v6(Eh<Y@C@x4$PF2XvQz*$uRVd2LOHV3I(^GIM$}cE@ zX=HE@fUNXFay<#m`L7wl`yD}AY>XKgK$sV@qzxnl-o*k6HwFd<P>UHfix1^8fCu_P z!48U2&?;3(4*;$Xq#uNtATbYOqUr&e2@+*uU|<2I1<;BOnBkz+vmo<8W3-Tw0|q`& zzK6*(FfcGdM)pA_pqh)U22`d%R%|nHfMyOrs~33~7+64S6F@5oL46~T382+#p!y2r z4$#UmP;v&<X`oeEAU0@qA*h4_tu_GF9H3Q0AodRi1_say9FRE`j0_Cz43K@t9gGYN zpq?wp%o&Uf44~!rAoc=A28Oi^4B)zX1tS9kD4l{<-fm!IU^oDB2m=ELXa?j6lnrvv zDF~Yp6xJaAO$eU@G<A9h$_8}<A41sRok^g&8N!8x4aja#JfX)0OdMqHdx!=O&<f$t zP&OzGenQx=_=oTq7??mSkfFnb9H96YhP2&TKr1Uj+ZaLq0x8yFWMBZz=7Y);11K95 z#?Dao0tN<#5a@pG2Mi1hQH+rA`M|)ykif{m02+w{g>x1oq)eQ^$iPqviDVAY>Ji8e z1qKdKnyH6~gLhAX(#|ACaJkL`3Lji<ng_KT<R;MW6Hu6d;(7-o0|Uqpp!hz;$iTqD zz`z1Z3)i9QL4Jd|8x(%Gp#A~*^EH$WG83eZm4Sf;)b0nRamY*{1H&Ii1_sb<708{S zN){vss_Q`h0Hu9UdSGK>fb4t#twRAN00ss=(6|!Bn;?@Jm_Q?3py_fp1_l<8-PTMD z&{hotgFO=ict;_~4tFRUWJdsWz8_>qBohMzXcP_<_6bZ34B!zwM#va0$j_j(1ace5 zk02XCeg=gnh+oXazyM-{(n%u|q^$sQ14tbxkAmn{CI$v41_m}z`kM;z1H(!tNd0{r zs^=jS1301n2Nebk3=9&?;CN#&V`c!iZ~d7e=Mm&Vrp*~Tm?7b^fSG{-R1WN9hJ@u+ zW(EdOp8w0tzyQkck}Qz?Xu`t40LoXv(DC0~7DyWFWno|d<)ig15PzIwVPF8o@mm(q z*d7A|J1Zm&sIf9IfbxYiD`bCo5-S4(C`{^E85ltMU;!%w11Jw1Wrd{m`>YHMptSj) zm4N{ipOS2lbZiE#&x6<)7(nq@#0J@CKaGun0W@;Ci;V%ilIl7eBo4o^LE=w>9TG>T z><kQ`bQH!8iKk+A1_n_1IGLS+0W>nZj-7!4G*Wwloq+)~Li?DVfdMq4`=6bG0W`u3 z8h!)S$9fzL;FV#191IMgq2qK81_sd1lV%PE22hp0n1cb_uG`JQzyN9oUFBe40F|Tf zIT#o~<GHMy3=E*1GV+`Z44|^qij#o>v~nBNUIVog3pp7WKr6rdIT;v0?WXmd3=E*M z_bev^11K%N<YZs~tvvq=$qcYE3zn?GbqmO!pfm$YLm<q^z`zSiKA?gh6jtn9;55Vo z@_#-zINV!6V}~HMAislX5C*vul+HnHkQ+e@<jT1r_B%jr1WgwlU}9jX;)aBW12Y3d z12-hD8<-gwTA*xDdh6weq@@GQ3=C7D><i2c40ECE2h0o%tGO8%K;_^EW(J1c+>kW( zfti8v5S0CqnSt>Hl>LdBf$2Py{e_u<@fwu<m6?I@0hIlXnSt>&l>MEVf$<xZ{R13Q z3=H5^?w}zp9tQ9Vg9oe(47^Y_C<B3(7lKxBJFqb@i19EmfY=Re3=Gmd5I-IOr8^!5 z2GHv72W$)sT09I4;FZsyN|ym#_OXKUI4BH3=?O%e@<7rIh;73INiQI_BM&64fY>0+ z%)r0`((B0sNrNCUKPVfN7C?MZngo?gAU3Gn0Ht9Nn~8ye6;%EN^FY!oNGzNO5=S65 z6f=S1F&UKc85rPYe<2UJOaa?niKM5S2a?7?c29w_k?jWMgE>6lvJBiV0l6EL2S9d# z+zn!b^n&;xw}SdX%Xq+Pkp<*dka|!afy#p@CQ#l1Gob$2$OFkYAh9hFyFg4BW`e9n z2fG36|D!yR{ufBW86HR;0<mvF{LBH$YmcC8P=0#`WrOnE8weW~E{w=^C&&y2a4<p2 z6;OTy#RW(lR0M;{cn}-bB>aXH&z!ud@c=5iLF$E(#N~Mz7(hEGki!eI!vY*$pduP% zJh*HEGoWq(mB}EtK*d1SBP*y}0r5d?F%YfA%fJBHT>;{O%c&NS8Y5mv-UErc@Ivw) z$dA5IHZubQV>pz}!oa|o0A;f>Ffe67+3XAqjKxqk2Ll6RHI&WCz`)oHWrK>JDNr^y z0|VoHC>xX%mO<IP3=E80plm({2FAlsHa`Oc<2k51L1Fd+DlWjl!1xu)7Gz*xWaERl zUx<N$Nf62wVPIfXg|bB%7#J;}Y%vA~Mo%bP9OOqRTY`asDGkb&Vqjn_fwH9;7#KUC zY#9ay#u-qyECU1MN+?^7fq`)^lr7J|z<2`6R$yRYyb5J2GB7YcfwGkt7#QC{*~$zI zjNhSb6$S=IR(^<IR2di;LCbtV^)W21f!gArG$PFpsb@j$CKY~g`Um&_b)e#)wxS_F z0|Thf11i&P_#tKS1yE!&Fff32!+_c`uKWxP;2krJ3=IBIz0j@a44@r0pz;Ew1+)tX z)W!^lnp42Yz>o)JH!w0VG(*{-HYcdf1hN;@hMxiz2esRmLfHox85p)e*%zSo3aI=7 zrQ`kl3=EK+I3QaY7$B=K7#NO#0)c@6+~x+!fbtMX{b{H<AhByuHYlAuf~tp5;P7Du zsej22DaS$TKO(U~VZ;FKQ-I`P?)d`^6Ob6A03^&oY!C*i0oAj@0+4V9i75#{;ts?H zVKWAX(Yyvt+e0m{fp%C?Bd>v$@DR#tp$rTRu)GH9V-G}LgX|y!rCM-)0iTS4D}RAh zfzm&|JcX==l6+(U%Fql99H6!l$V^aM0@RnXhl+!CmU)3H1O^7Mo1+967$CdL7#QNA zY9PDNz;!8DO@;v2k5J>l6q73>14EYpxINdx!OXzWFTlVM50+wJm@UA-0NP*gz{0?= z6sos@g@Iwa00RSPsD1$p1H%ac1_sa`jsq+V3>TnoIKaxla1*K?)Yg0|z`y`LdWwyK z;R}=vYE!ZbGB8YIU|?uqXJ8Nzgt*OtgMmR&5Mq7;s2?H-DZ3gt7#M7z>;)VQ4DL|% z2RIlQ0tFcuri0uJ=`S$M0HrHV28L)s1_scKcmpQ`L#iOeFQ5)#ksu_j9Jm-58ldJR za4|4+LD>yl3=A^`A?@7-Tnr4$1tD#j1E9_%0|UcukX_si3|j;t{&L`EU^pzuz_1r2 z&cncP9%_yP4+FykK}h-Oz{9}s9p*0{1_sao%|4Ldc^DW(gdk%;2Y475WQ8E=L0eih zp=<>{1_nbRh#MUE7#JL&>;^sthF~EEhW!i-3<vla7@~z37!HE`&d<OA+PeZOryKYg z7)pg8VYq;wfuU81f#DEHJwF4(WT^QM_!$`H3NbMBf!r^^z_=F5W)xsx+y!Mb2{168 zgtD0h7#MFr*(?GKEKi|q4gm&+uR@Tz6||N6FO;nyz`(#M3=s#VYe`|KUj!H!)P)%s zK)e4M1Q;01g&7#;fZQg)z~CVaNe`gGFwlA_kY5x)@g@vO8xDdD3|UZif*=D!tuO<_ zVNiGqGBC6VL;SKpkbz+Wl)XZbfnf>M4I2a*7<LFVFtmZ}5@cXF3bhLqR~LmD7><F$ zL5P9jt}p|`aZq>)F)+LmW?%rvmk<NPH(^M7`+yJw!+)rmpn(o95s1A4!VC;zB9Jr& z+VU<BWjhEnFla#4BnUGw7(>|^!VHYwP<Ey;17k9joh8h`RwBZ{a1!J%VFrde5lDak zfG`6?H<S$;keDvQzyKNt1RWo<M1+Ci49H(13=C^U7#Pli{36Q0uoud95M^LE4P`fo zGBDhNvJZ$dFuW3BU^ov_FUG*|Q-p!xBFOJz3=Djt5OW&D7#O5P85k~u%ok%|&=O@} zxB^ly&cI+P%D@1c)o~DKU;x>16{JR-fx%mpfdRB?@PIf2L!>BpT%@G~qz5#92c<z2 zXa_2&-T|>eyH`PNyi`%hdOT47t3Z^20W>WJ8aJ&Hg^b6{U|?WrfwJc?FfdMnvgd-@ z+fepA1_s7;qL8!%Y9E5k2kjaKwSD%BLed_n4mu&qzyR8v3ToF~5M=<L<OFK7-G#Cp zm>3w|i9*h}1dRoN%mFViW@cdcDaybA+Ia~Y7XXQac4vavJYo>{gT?|Bp={7t07wnU zPoS{?Gciax0~$bd6oZ(f0IFaZ7#KjuMZ(yidI!{h3=(5tfG*zw8N&cwGQ$uL8jNIM z0FQZsBtUINkoqh!2I%Tb28I$S8#D&j1XT|jw}Pq%w-r0ZAY}zeY?2rQ17ycAhzG{t z5d~2D668cso&<@4+y>$=5My9~>_`TQGBAL~$3d*Mn0A5dqa9+1Hsm2ONLw9Jb%At& ziwrOWWIjmzlo%u}fdp?Lv0sUS;}_l^hwKh!VE8J=zyR7o3`<v_Iuw)^LHa>#m^u&} zG~WCh>Q2!3E=Ub%SQiwAT;dE2(5Y>ZFbFf*gT{G5jXUW4gt#~Z17tTZNE+NxWd*re zR-AzWvip~TK~0>20kT7wfdO<x6l51ML_Mev3Sz<fwjjMAK1>|cw*ZAP$UVs7pea+3 zI0#!oB9j9&CJj;pb3drw2aN%M#6TF-cL$l{Dh?@!L1O;mh%tv4aRvs+&SH=Z7&F;I z`z$c`fZ`rxCs-B~fD8;^|E7r};wWF7fdR6EnSr5B95s$W^CBR<pg02UN=A+&m>!U^ zFm)g|gY4^vm;=(vus|GA7l1fxkl4s`6(Bj7-rY!Qjv=wHBe6m1LH61(GB7-Yh9_t~ z7$goFs{-YnFXE8810?nf>K>3dXtE4cK7stg3~9J=faiQ95N$A!9$5JYVuPk2K>iY! zU|{G32{14)$RerNl3-xyf{GhM&Fp4iU~ooK1CoZEqzPg{(jLf6UkOMZ1QBIm=!G#D z7#RAI*r2ik6rX_-kopTG6#->~(h2gM5=aiD9%O!k1f)&}iE;2i*r0Tq4|OMKdqg8> zfB`zjH&Fr+ZlEzo(AXF#tfonT-3%U!1DORqmWyGr1f(qlDl1`nVP!b1OaQ5e9`nWk z8fS#f`GUkj?gY69H0A~x-v)_+#^^w7ki9FQZo{Vrqz5$50rJvTr0@id|AN8{6z?Fv zz{~`N1BeZBGe|EeExADR4#>@*@H`;Fz%Uakc?^jS6=L#$st4(v&A`BLK>|E~z@os+ zz;H(bQcr=@K9*nr*R3GGyo9nr^%^LwK<y|{z4l%LQon%ewQmxT`~n)I_$2{tbAs%W zgr*Z_1_sa=3#e@mvWra;q8?nALD>n+3=E)t4QL4vXzW`^l7V3bsICX;m1JO818S?V zGBC(XLgq3-ZF@CI28MMYaSjFsZAk`(^`LeICj$fMSiKD(ac*$C0NnoU;AUX3mxQFl z3EYsmKTx}L258J2D!zc5fx%A_lHN9OGcbfoGB9icxq+L3Ax@HkVJj$bco-PcB_ZJh z%9mA=koHM~1Or1IXmW;ufnzR69b}@J1r#r!yfzOi)(%xOA9OB)B&6&DsR2pD(%@W4 zNZtgc0gyN-enI(SIV?Ouf(#4{(Cy4%9w<$M)U1K}1thpdl7RuVbpvKTtc(Tu8>ALg z>F$*TkF$g42~WWM#lXODP7+dvgVNhINd^Yc(Z8TH0Gfvb#Up4w17rs%EiHvIKyxKf zCIfUy5{SzLnuorR<ky#yko*Udd<|uT{Q6cBJdef#8pr-E$-uA@qyQ9-QV=%CE$mXz zx*cR0lr6x>z#su-gT~wBplr~*FDO1i?Fvwu)s%vaCj~GvFqlHw5sVBBj!-seoWNZQ z948E*bnP$2z_6NufdQ1RLqHR53=ABrpl$+{{h<7vEQM$XrAaX`!0O&iDM(unq&yd@ z7nIlQq`+rkvVii|1S!aT9Z1arC>tcc0m=sX_Yjl~3i~TiHYn`hLfIgHu}MSB0l8To z$_BMDEu<lF3kqA%d^l)s5M&o<P8!4p+2sXQ53(y5$_CjLBMlA*aM@WbjhJUCmxkma zkop>FNcjpH8}E~b^jSgX&y<Fi0SpWbOQa#`2sD_!1~gg8z`(JQfq`Kogbka610_e0 zRwhr-`UGk4yec@1K=a;PKr%?_0OY=d(vUVV$p7c0A>|1u{;t5>$H>5N6Uqjyi?|PE zgT_=}K-r)*5+9&?L2>*Y$_9nqKPdYFs3Mht_!AU=$mx<-1`_w6uz;l@SRRJuwat)u zEYNfqgP;r~OhM@eRCj>ZVSv0QFT=n9+G!1n4-g-er$Buk7<(s_0ZQ^vCU^}8lnu(p z46~s2fa(sAIH=43<$Dzw#Qc|`3<JX+kR$^GgFR^Qoq+*dK7r<}K=ZF4DR<DK2?hoZ zkU72(Hhc~z2pSF`_s7V<*N!kG%0Tin$o&~mHmIKkG7sbzM@9yQT!>j5AT{++HYhw> zp={{d6Hw9yx8Xr;jH6HkdZ21Rc@Zka<OHf;ARH#hYCkZC%Nj}CosofI5|W!2%P=sU z1}kG=SS15VGa&bDl7W=@$ZobmGRF>yeGaN0IW9nR(NJL~*qi_pblw;w4wDDf-yl9H zOfEqgpd|w^ahN*DE-FO4fz-Ppxxs>wfngUkOs+xAfkr<=kQ^kfg4PB_$}uq9fXXL8 z*`T?POgTsyisWB#8Ul?2f-)j#y-u_Oc#T5~Xk9|00;Jpo*^vrmgW4{+P&O!?6)Hf? z1f>m7o(1(QK;=#)R2*b)6I4AY%t7-Spgswx>}XS9V7SG=zyO*T0Xg?Jv=83{G5~EJ z0c00Q4Rj3>!xRMuhPxmEC<fJoAT^9g<uWMGg8DNM;~;qupPHMB;B_A@ptS&?b_>Xz z=zf2w$iQ$PG|9oh!0<{D;&+hw-xMKX1@iAdC>vCtft&{#w+8tg#D<j*AhSVX4vqj& zL^Ck3g3JM_fv&9r`|%M-0E$6=1gQb}5pwbq0|Us9AU3Eh0x2V;W}-59Eebe(XDCD5 z57IXu$_B;b5@ksF2C{1flnrwK24zT}I)Q<KWh<1O!oa|?3(C%5U|`$_WoI%lFdl-k zvlti{k3rel3=E8?pzIvbyd;#J%fP^R3Chl6U|_rkW#=<6Fy4Z?4b+c@r5TWaA40`J zeI$?{K*9Kwfq{Wr6TF@WT!-OWp9E@egUTV0IXaq<G67VE!`38$>Kc%lp!^Oh6G7`h zo-i;l*l2?LUOdkj7#JpNLF!phn!**9AbVhW0_Jv58UdLNa_=ipg3t%A%V7c82~rQ~ zCNeO9=958eP&|Oz$RIW-9zbh9Kx|Mx2IVIZ8{`krye^0h@&{<H62u0X37W$Mu|eq& zl&(Q+P=U*<4=KMvZ6{Fs2P6)vS3&coAU3F81+86q4Qf*}GBCjAMHQGB7-XS(LE~7U zH6CE~ObiSfQ1J#P1_sbPAxL}y6Qq3&VuQxI454ZsFflOLK-r)LS#J6a3~xYpzA!T| z1VY)MH89aoHK4JpbSN7%hfoA%Cx9{%0|UcbkULoz7(nY6K<ymR96}9L4QT6RE0nDO zTJXlezyRvkJFqe^^g+cNSQ!|m>qGJ}XpPlkeMtKXH2(mNFK~7R`2%Dn)EJNyia1CP zBnWCkfYQbqeFlbiAOQqsGKZGyFg2hx8lW|Z2qg?mmIyWAwMg6a85mwNFfi=Whor%e zP{pu12PO}i`vR>i6ow=;9_ac?aFm16GECiFD~LH?q2_?n<6|T?s4f9<LFR*Kd20rS z@6h#=pf!?8-~~<$JkT|pASFzH7#J98Z6Ns%<af|`3(Oqw*aZWm{RZkAfZPa5+psmN zpmG<~&j6{1I31k7!0R^JZ5Y673qk1%wEGs6?m_ye*f22sW?*2L16qv1z`*mDfq~(K zEm$85XekP=e0jkZQN~>b4Q4Sy*16mRv7vM16YU`8fcydKzJbbYkSAx_LDC3F9HbXi zhk?q?d3Fp8Owe_sAayJZ4B#=)g?0=Ku&a`m*+J|D$*%*M4~^r^b_@)xP&P;oRE~qv z5v)!HiS4n2l-D4;Kzx{*qfl{>*f}U0rUq2JfW$#=2bl-L{0<0zNjO0K0#c(4WrO^s z=K!gjLFQOFK+etr#T7^$8`NL!4h#$&P&WrUFfc&3FEB(qApDZzz`(!-Rg>+&z`zY< zmpU*o@IcuhGhyxo)z=_5B1wVs7N`sbiG$K_tplVk2k|={AYlSB137%AfZV{q&;n8e z3R_T~1#P=n>;NhASQ!`?*Ff2z>xi~N*`T#Jd!cL&1_p`aAiqP+fz|6EyFg}w!jyx7 zf%ytF+_)JSm>xjcptU%!p={7voR3g8Xf4i9sGC7y3o?s=fq{z;;bBhDuyH<U!6E|# zA1`$82}~XIs5uaq6O{fdpz1(<VT2J3Tp&4+B3QVB)=PuJ0+c7(p?XA6%?5`vC=Y_v zfp8yGofxV*CP{=vT%h&R)1m4>`(6-I3`{Zz4i~7~Hy^4_8qGX;Bz2(7z8tDf4ow}X z{|faJ7wAfu^^TBq0IK^y`2^%Qko;ap$U4~zj0_A%p=N=$6eFx*U{XVHxIpRpEL5E; zs-0Y*@Bo<!3r7vmAUFeLz3f$}9!-Q<Af=FSgsKCryL|&yr;SG)s4XG_S{TZ}z^98x z9Vl&RLe=TxQ3oCig{m`zstb1lm&>4G5e6x!7$~pHBC$bj0gxv^;wnhu>PYMuCvdof z%>k(csRyNH$Y~akwS7rW;PDQy8h)r=P?~}s$pos47(}7spm+m~Q-IPeXos8*BY1rs zc+IjNBV>&!D11QUI-of<P#A!&C<Be7gZ!E01g^)xc8NpH1ld&t@jrM~X#)~_5)vEK zHw0Zl2J*vfB=H4E>{UqYLr84UJPu6nStM~#9~mYNTBi<UgT@MAY|t7582bj2-UmqR z_ekt-NNfgYkZZwL<}om2=9MtS$2$fEIY!0@dq%m&heQUr#>X?1R2HN%<R(_cr{<Lu zWu_K0q+}MQCYQv6HUJeffXMjF6c`;}!jM{$5noW0Uy`4kp958!nV-i{T$CK2o0!ay zQd|P2LHx`DFgLRREL)IYR08IM86`!DdBq?W!m!Nb+=6(pbHJ?R{FGFN%(VE-yp+@m zhEhW_hWL08S(;aznVy%LqEMUxGO^SGByPa~7f#MdEMh1%HUbG78zIC&0RZ++ZfZ$U zW-`=oP)>XaLqTG4c4|p6LsDf)YB57`Nn%k+d~SYTetb!0Zfbl<F+*xz3aUU_PGWj7 zXunlnK0`@zK|E+vRxv|LUU7U;YI1&23PY)x3CI;@CJ0yL<maU;fCz++_!5Sk#FErv zP?&=ZgmQ{Y5=&BHG9aVllM_peQ<2P%FJZ_`VJJ#1M0HkCaRI78c4{R<Sz=BpD4q-A zA+AnLPcKSMPb^7IfyQ)6QDSmxd}>*0UP*jWVtIUaT2X3hd~tRXLuy_LLveOeVoFL8 zLr#8jVhJd|;Ypw<HL*B9k0CxjC9x!t0cuKKNl_(3K~ZXIZb3<Wa(-!E2}40<3dj_2 zAjLy91$#Ti`}v2&y9D_M1h~4yN4kbEfW$!j_#juuV1GZbd~krPv!|P<s|yy{_+Wo$ zZ!G!(yhGwc{QcvDeI0##F!Y5u2gExEhx%ejKun8w^YjUE4Z@HOb-|&{&ou<11?(3L zef}XHu0bCD!I<9j#9^RkK)k1)hYO|?JOko`0w8u_SQF~!>FgLB67T63?_(728|o7R z=3}Ufa0~K{5AgT#bdJQ3^9+dh^A8B}5Any4f`(0ybC^@8n;V8SSW~>OD`udALfhBH z)XzV}%|Fx+GZvxpp?<Cr0j|y=SmGI~!p+gsCp5?v!)mC@oE)9Kef-@qq(Qpfd>q|_ zvC2fb2Kk43`nmXrW0m&zafx?v3~|IR8Ri;<Pp_N5uWOJyrh`E)3UYHcH#Rbk4{!{} z?l4ckFh?Ixm-t}Uz))8|XB?)u24hM6AoJncBmJ<t-Py-K7`vHnj=>@R0j}7!fx^Sl z*&C~opbUpk-qG1RKGZMR)h`53kh=ND2Zp+ay2b|th5BK4pud|x_KfBk5bqxv;^!aa z9~$C{8LCbJZt>3ULH?lu@viPcuECfpJOjeaTs(svoqVvQLO)kecaM1IAXmo_S5Q*K z2m^=$kQ7ecV6VA_Vkwg#nq6ESv1Jfk?hSPD40gsI7R~{o@wjqCgi8R1<3pU|J^iqU zN~oWQqo0e9t4lmMlVfNF724Q~Q*Wnu_hA3{U{}ms3DOkm>>m&r@8%flgIP4XxQ4}J zD*-{`L9UK2m}M6zSmT2{{V?tF^T$>$dAJ0{hXjTCIb(Pz#4!L@9RQLFiNH|;dpi5# zD$t>s8G8VNi+e{Gmmt@mApan2$ui#44`;H5)pbsum_GM)j9>sai4u#_iy08LI=H4T z$j{6x(J#(U(nm<>>Fa?}QbC$Nyj7!@0ct=hz*T7!mlTyImneWu!PaU}&;r?~sZh<J zprD`-AD>j3nNyOP7oVG&Tbx>=p;nxoq@!S<qfne#m71TXQJkHmscFq%U}XqapHz?r zY8NJ@f?5hGP}MM{5Sxp$<C99$(lm7xKo;rRL0c$bLrlPiq~?|8DimiYAvZY`AT19C zTc|Dw*BY!oGfhDQ!dI|U2=;aY=PInt5SX{1fl!>EoUNc$oULFBvQDo!+nNFDI)>yV zhWNa+<PwIQ)I0`I3n?DN$W2TJ(-|p6V7jn0wKO$8Hxaypo*_OyIVV3aH6=bjEv-1U zgaN{1$ji@2DPl+i^B4+JQ?pZ37&1~5Q{vN#64Mz9(sEOC6LWI%lNkyMK<&@U<eXH7 z#N?99vefw0ijvg4;>`TKVuq5$%p6cl7OWNQB8HNp(o|5`GL#ml78NrfDK1LN12w{P zGK)(Xa&qFqoV?VE5>V5wj3FfzjN^-wixNwcGf>;O3`I#0_3`mVN$~}VMWA+Na(+&J z5lE&eGd-gO#K=iaD`5bc9}n^hLk^e;wx>9oA+sPpB^Bg5SV)u<mn7yEfSZxIU}k(? zF+(<_ea=t}Zft`{Q1cqm#x5z&F9J1l6Z4WY@{1Vai?id4Qd2U)tfcs&)U=f15{8si zNL=O@fdd8<4)O5?*(Koq2+VpIpCL0nFTW@?J|&GIFD)LN1wd{sF3rtNO<~B-&yUZ~ zPON0eF^SJeEY4skE{D+NnQ58vpiV|gd|6^nW(osBfFUnGJ}nv4#Yj#pC@#%`w6Rn3 zk`oK!L5U=}xHOjmL_l>y+*X#8m=|9FI+r1@1m;F$F^G=j{M>?^)RNTr;&O;UPHI_d z4x}fRng=@UAwD@jFD<humjT?}NlDGgEK4n_1owLwN|NJ?voj0glXDV_i!;+IK>~>- z@tJw)MXANb40$P;#mV4^hIHHt%8Ef5w;;8sq>>>sFE6zS#07U2!9mGTl$s0nM}Asb zPJUtv1C)+WD^H2fO)ScW3*;Ch#K7^Dnp~2a!jM*!pNp_P4dS>|h+|SxlZz?~K&py! z^2_7Xi}E2J&dUcmIJJbKI5{^yGe5o*bZQ02Bap6TK~ZLYQD#XcI9+9CrZIqH4H}U7 zCHV|sW=V1;12})hCl{156sIPGT?_FdqB9Fh#i>P5pXa5Pl;;;^gOfH~5R~_!sxY*n zX+Tm63NwcI_;PqU0QXNJ-h&ns@$pHihGq<Ti3ORU9(!?WUP@{a*s)-jf|5y5S!zl= zs01j=&(DQA65f%6cqOv{EC4IOz`eTo5{9DGG;lW#nF+};AgN-8#FG5n%;fkI2C%Bs zB2Y<gWdJW$L1m+@f?ueQk0t}Cv}A|}Wy9qB++5JmL3VsmMLZ}Mg0d!<6`z>~i5rAS za(;exW-7>CIr+)iAR5#sE(S4*lFM@P%M(Gp)c91;5h?ND0u?Nknw$zM0pS8kiOJbH z`RO3JwEUv-#G;gVaIAs^;6ap|nhWCR<iwX2rzV4#@KDW;FG?;;Doq0kre)@oq!xi# z1^GFd$)E%T@<x1eVsZx9rTMw3xd`Kn^HQK1%R%B`CxQCJ@nA086(z-m&^~%`c6@nJ zW=Sf-JD_48l+!_WLZk{HSr{Y$8hU|p;W@fEJH8+>IWsRk9yFX#3J$K~ycAH8nGezf zVnBR=$OMplh|pOC8iy#(jt31AK@EcmfeL4kF-7^MC8_bCvJ1pZFV2T#_Tub#5F4Zs zT)`A)LqY>mycK81msEiB8dP0MW^rOtPAWJ|vQvxl;=xI!I6FQsKR!7lH5sHHWImi* zQ~~0Gsx45O0DBJVz~YR|G;n$V7fT?&z=o`#c7ffLnhw^JlbM&ES_BFTka3{&9}hlS zCm$R#$%#2Rpi~nNvIC?bwFoSpT2vHYoDZ=jF*!RPqz0O@K!&3Um4b&q;!7%uQej>$ zC`wIBEs9SnEv}3&$j`|EnGNT}CzYn8gNm%|_=42bBDm#AnRzMk1sUK(36_A2t3VVb z7eNBAD7g&e8*mJR>q(H&CB=}DpW^KJ;*8Rgl>G8Mh%jh)s5m>mDjCX126+_bT#z~O zDXEDmIhlE>@wuSp065;j;zg<85j=371BYZu1z5Z^ue2EA5>O06d;y9jaA^Xvv^X`n z6k6oQC#M$`z$J21bCbcD6O>$2<C8NI^U}dt3EXxnN=-}w=Q2pz2cJX)G7jv3c!+C2 zd}vgFvMPuRu5A-bQsXOfGfTijTE*G%uo@qvpeVJp7~+-WBoH6e{46fXFG_^iU6fqP zkXH<@=(8dD9TW>j;A#(CoW;XSvgG)*#N5mrkRy}9Sq|j*<an?J48_?{7AVxfyv%|! zv-~`$^1Rd%P;(?1JlvKXpOjykmlB_nS{9!PDs7U%6)9L(9ymmk<IBwAQ^2EgAcM=y zK;Dfnh6}(8%;fl#{JfIn{DR8(q}23GP^>4%msCJX6?m%?WJVEI!PGREli=p0=7BP4 zay-bgf<#bT3c&;!gm6|-av9W3p!&(!2<!o94oC)#=YrUv@z(6*cu=rFO@lMRrh#~1 zjd`V@f;l-J6#mfQhw1~l1R|1wXhRkh6s49i6yz5dXC~#O#)AU5gaH(KnR#Yl2Qz>e zr3|GIo&}6yXa-{$8$qi!Gn05wd6En-nv>%}_N3;46GU=+NqKw<s91nBhM<nk$tgy0 zQz={k<gLuS<osMv(F|@-fYL^CS$sJ(WJ}7y(V15Pv9$<RP$tKhXXd5kmxCH);HEIx z>f*eVc(BrZ&=@wjumCw1l#v<o%kxrG7|IJ7iVHIHKq4R+D3hTGB)|X~?}f>NIbhMm zqU4Ntm~eavLm`X}8cYU_1cQ1Y&<Y32jfZi|6EjPo2@KTOi7#P569H=g=e5$bG;j-v zp%BzHf_9w1fy+<?swd$UBghNc@#Te};c147w4z*4sR^ofD+)kS%aBwFYC|MdW~Nkt z8YQ4aTaaJO04kA+7}8VoGE*2JjSA4DLJC81YEEi$Nq!MSPFe{=a%mBWDo@PGW+=$d zDTEjnUzD1eSDac@0`5rV=Rs`ED`J3Ua#%42c1>AoGE`e;UTQ^RatYKE$)yZM<)9Fa zFJS<ODV$dbVL`$Mq=F$iucVj(tT;7=AwE6_Tns|I05J_TOH;{^T2YW$lv>P?R+^U# z9*ze)0Nl<iW5|U#6Yf7~W(4IBh7@pC0+aE%iN)Cr;5irw2@wF*N|_Zosd@1!pl)6o zgk8)4suuGZVC^zcX%4XlWNd12G01Nw42fo-9K{eH530UXbK*-gQ;dz`OCTz9GSgB) zkrAJqmd*gGIpaZ7E%7;-xtS#l8K9OJ5+5uNnUX<Pfg%D{lbDhc4-Jm^;?(5)yp&>y zVb~<VI!cQ{Jz{VGqUyjV0S;J%%S($GK<0wH0qQP8`bc0kkS1U}C<TG1(kel{1qM*f z3GqEt9^C3<$jHx0fn*N|6J%ZyIGXcIOBj;!^K%&Doj@em=#tz5P@f&t8H6UhqSV67 z6b49boRL_R%8*|GN(PBJAWIW-(u)}q6LZq@iy2ZOR5FADn+B@c3o;qt%^Fbt1Scd= zZUprjAh{7V^iu?@B|u&EqWpZ&3}Ugd5d)}$%}|h-o*EBs0D@Scej8}80^H8aODzG_ zTWP6@C8b5FptO;mRuEspkX@RY5?{gqAwf+8Q01Rr3a$Q=(~I&;3m70hO;F;?%m-!V zywnn;%GbckzzQ<FgE^*@nSwl|l$in=Q38!Vm0Bo(^O1rrXv7JekdyOsKs?C6PpP4q z0wiETVmjbv5{Lzn1vLf=it-U-QJ{G__`p@Eg{F>zMyZ8@mZn;9wyqsg$3_oS$b&5g z4@wz<oNjCZ=@X^sfeIne%qC>e2t0ljpP89v%>W*J)6>&WDoE2WEy>I&*2@6v!7_-J zT9Se6<J6K2ke3atz*d<WL$sm0AFK%!P5EgWARRgiu))04l8lU$A_Xl?&~Tit9VlTa zXe$(FC+XTDx}s1Uj6oB@paH22a7&6IBfdDlv?w{10XjR(U;yp@p*lOcpi(0<Mb{40 zSIJFG)=@|;$<Tusp`+j$;t}uY<Lajg33%)}K@-bJIw7{A=ryo1g7_R{5%P4kf~^85 zF!D<BGm1f8*MqthY9DC)4^xXR*jWMbo&m5?KTwha8-$!HG7I2IBC`OLBp|(Gh0Fq6 zL5kYhhXyMsjhI5653&bKA}>IW^2`E|MvzJ{Mo|t6Yfy-RoR?W(l9^|Q<V+Bkuy#;X z;nfcIB12|I4ntXL5onZ=p#)Uz=9hrSHbEr>xXW5nlEVP*#eq8irMZS?ppvwxn8Cox z(h8nfK)%4^2~cu`3>ZT^qp72ymRX<&ZuBB~3?cyX8pLqOpfbp8>}d+(MI45Mt50OZ z!2%G&%^?X5WU>}gxsq9+2jfALGDrm|?Gcw`%FKvQG@vd7C?A5CHsmDbFcc(Kf=1TB zfe#w21xaS)7eH%K11mF7XoE1q?RY|5M*$YTdf>LQ9;~gbqu?13@9P*556Q3)a}9|w z7ZTWDbHO2r-&|0@fiYV6LVX4aUQo`31-OEut%7F&c$OtT#2K7vVFe;I8KB2QNirhu zmLwx*-jZaTxfgxx4wN3i?nDVo@Kgube8f}-$O=ts(DDyZ%s|R7@c1)BacUt0XxIQm z7o`>(GNk0Er7@&|dTf<>3`NBy3<bp*AQcRyMd=Kw$*By<<wXpT{s2fQzM!NC5~<); z9ZGRml3bt%u?t$-DS#V+7-~S>Vwj5JROpfv4K=VIz!f7*5>!%xq7u}gRIr7Z28j&? zkQAuUfl7fLgCvCrI(VGfD!`afbqWd!nJIdZrAV3ic_0fiix6@}sU-@wpo5Shb2~Zt z`Prof@rijU@!(^PQ^9LmK(!dCd`QuORR1~(U=Qd(=YU`y0cRZOG9U#l__`Rd5zyet zO)Q9q+L4-*nyaA(R|Q=?qoa_S0y7Vk7~%2|Qxu_NpU~N`QZo~U<kBMWlnAKh2ekz< zk&&KS5}#MB0W}RR5TNt3kbppOcwTBb#4cN?C7{Hp2llXnE!5+Xw4tD&pa*K)frn4^ zAPwqb1zQC}9c=Q*i;+N06>#H+!NAH4n_^J=9b^=^$&izp2Qd*6S)g183P)%J;R;7J zP$Nza<{KRa(2TvKUnDFz(iAkn^DXhYsl~;K>8ZsEYHA7~3lwb?43H~mkaYz`nRzAI z8cN{dAy8^j0Qni~vDq<sN}zO60<{MecnYYIsB4EAXxg?4hLE@*paBvv+P09!R8DFh zQM#aE4{|hQ?U#ZrH0_e29VHErq7mG5g~d6NMo{Y?9OjVb6cLdMic{3kM2}c-_<?c` zaj^?ljwg!2i41lcxq@m6QuNj`K&(KlbOQ|wfx0UtsksH9AcwAH1C64@=OyN*LN$Pt zL30a41TuM^ms+9^k^-3k>D(bV;6aN4LH1>*7D1=bK|IJ%6x1+S>l-40+gYeH>bUwW zItn?dpyJX15@E&VnI*{?3L3ED6`n;ftEtiyl$K>_3ZiKV328(Z2&@#^g#f8TbRkMp z3b6Jc5M@57SCLu-sxC`YK(#BR9Ko!NN>d8-P%0y6Z31!&qy-OBfm)>%rIwTy<$>Gg z2qihGc}Te$l*T|Iqzhkp09j4|ag3&dodRf$pRZ$tZ?HQkuneqBAufcP4XQ^$)v|&X z%z#p3BdGZxvq1%RaS3=RQ^D3&!No5)KEOXHM4_ezRG}eiMT8W{7I1o2Pyo9exrvyP zR}61?rsNeP^(#OL3lfw>g|~t>C|!U;9cC5Cu^NcLOvx*T)E1fwYG79f2E~Jx%xQvr z0V-J)^tBXH@`@D-OH+#~6}0prp`xIm0Gg?b&o7DxPno2~gB%8`nKU3JIm8E`E+nWv z0EGd(Qis;Z(7*%fiKkvQ4DudokpuDwcm@)wf&{5b$t(g%LDqvIM+WFx1#q2;TuDN* z6*v$vvk;=%LNxbaV#r~SFd0|j4AZ3t3QovsV!g~1XhQ*#wo~$ob?p$%4v>?u>4MpS z64knPU}0#`gR~-?1gWcRLFom_vv8x)S3xV-qO@hOX#vkwqby0rsR`stA}bV}jRaUu z1utk$$twmgAJVi2bvq$zh8Uo$7vQtZCCQ-S^y1>o6!40ec+m7lQEF~}Noss%feC1+ zIxz>#f(x3#1<gQ98bB&R<r7pC%mmL^<))S-GL&Z)rGmR-pvll;(4-@nky(|>09nxj znJvvIhwvba?Xy!WK~ri;aL0l(NI_yzaVli#0%Xkta%BTjQLL8?&Y93a011IYKC>jh z2+|&gHX${wA=c#P=YixH^h!YMdO%fvX0k$N9%!~tAv3Q8e60w=07zL09^(Qnz5*?J zO43m%wNQX(YFGvW<*eew+yc<tDyU0l0BWd#%NtMwx1cC7FC{-WzSP)ALsLOj0j5$D ztQ2I1ft4YoiCLDPnWCTt4$Ra%P!7<5h-hkn+98_YrY1}lynqZ+UPBatBou5FaA<*~ zRgg<z3Q|il5TgKEpwTe}I|Yy-pd<lu7AUKr<a^j43#cmu8<x_w!!e?TRVS!UK-LMV z(ojaUK(>N9Rrm(8K$e1Qd1$5vtv*UA0?k1df%-}?A7&OHc`&m8su&aoC>n_LHOTFF z$A&;I$LecPc#)<P+?UQViO<YS%SRppg9^dQSFE-{+==ix^5~JS9s2MQR0OYy5clIU z5j?N~uemb|bnU<*FcU#>iD(glV+}GO4N?i?LQ-d1W=>9gjtR)q8ptuE15cKqkrZ7! zm@XX!HCTW^3;@{$-cJKs387%C0F~8)i=in4ZM#8N1`<Ok0|h3ighNsV6GA8gg%6q{ zm=Hvf7PtmNG6T$q$bph0*yE59Y0y|LvVS2h3$S`kP}L1u0__;&f;WGHTnI^T(AXtB z?1VKXfjj{-2OOt_%>l&#&e%p7#{&DAh{%R&!iZ~(+y>4w@Bw%5z%-&|0vn-*&V_># zr-79rbkP>(1O;^J4;n)lgEOh%iAabnq}G9Kvm$>;MGfA#0i_4D#vIy^3drZEDs=6j z{VN63UOKueNOK2k*Bo6bIK?T{K!;NxUd3iH(M=>!4Mti|8l3b|-G!|y4GJ(wf0EER z3CR7RMi=T>37-BlNCOC?#4)UOsDM2B1X86330}m&2V_VHBt@p@aW?xvW`g{UZ8Qib zhZ-iZ0s|xq!<b=#dLbIpNCHd?h=v7#UTRTMUcQ2@f^Jc22_l7nlz;;a(v(-wg3^!? z6wq?iq|&r_==wtqHArg;-1k8A|B&=+>L?gMYi^LqAPn-kBE(c|-U3Mz_B_ZS<N!z} z<L);lG>1Xl1nQ|M*eYbEpoR%d64vjtg?B?x)qxxZ3#H=Hq}<FB4Tx4qKtR_2g2M~6 z)Ev@6h6v(s3`0A8)(j;X4DiuzuoiHDW$4-^CTD|Mt|b||cEy!>u%N9~NX;ouRluSs zEi+Gnu&Scs60j-*E698>nt5rNxNO4f1|0Sl6_<cy46H1{K0~vspg02}YY3ShL6a^m zN{32AW+l<2Q<GDn(vaC^H0k8>B9Jt+%>mlzY64!*4(<^$K!q8QsyzlyoFi7CDS<Qv z4Ol}KbPJ+3Yz#&VG^wIsV+(H-fouh(W*CNArE3Rj2SD2;phN}fN|z)T=-NTr-Ds&7 zt`Ai2W7mf~NCmYHo|H7e^Cqws8D_5=lz?C!Do!pbK<<5j`I_jS0*j^;F_e`grbDJv zK}*8p<8xAtArS<10<4{f!^dD_$nrB(Cz`L3JpyW3B6|eP$M6VPG^L0E)V5?O&0|PS z&d6r~ZAdOptYiR98Z$tPL(odC!qU{d<W$gJYRJmc+>%moTE_4!$W(A|8dTUM=N4q> z+JV}=sHF)!Qo*W04nU{|@erz!y#*TDL?k%SuqS%lz+3~O;dWq6aHT2aB{;A?w4eu< zLa;;x@{t*O;G1P&4SmogGXq!yIK-e#PyoP~*`TxqQ32`%m*z2)=7E>$g7?*fS)iqo z5HSme(s*zrmd59$#3!bdl`w!=MTubJ%FH0LnJKAxC7EfNpbZhpxdmlraMk%`sYOMZ zDX9#_pw;x5pnV+Rb>&6ylwbriDK90yD3Jjo3)%sb#*mSpn;M^(4%(}pUy_kp#E_eq zmzbWK!cYWSh6h=AMqH9H%h0uh_?d*1gU|#DKWhfqfCgly06tsA0O@!##OGutr{)!> zGC)>J8$fA8C~X88n}Jku@g)q<>Jchy45dw=hQsEi^y1^=<I{3WKvT>3=0)M^z%2^s z01#e5Jwu3t(A4S~G3X@~7c=PPCKePk=oRII5(xv$YoOv4Yy)^C3&aB*9*`3cTUZSe z0j&XJ0IymHm9LPsjhUd?6wtINc*ST+YGO%7d{KTm186NMbcq;9MM`Q}W-@4XbrNWM z8)O+ac$qb*lm>G_tE3_8Vap34Yw(IwljD<1z{iw>?wAFg=K?w*19S%f2+K$_Ff>Rr zFx+7SpGm?X2)axQq<{f(kD(vrOgFH|a|Q;6vyk((!2D03^T-UqXDfpFUqI(qNii_| zVqjo+AkDz=4|I+g<Qy%q{5Oz1=+0Fx$l3cKK`kx@1}D&6A2N&#pmS0{{3<R6238eD z2GBWLApR;Y1_lcqM(}w`AU;SR2!qb`0r5fdtU8Pgpz~Qk{P$c844Nj444^YnKzw;_ z1_n+KMh4LNZ6JOiHv@yT0V4zG%v%t@40LCP2?GP@&M6T8oD2g4s|y1I==?Si|A!0% z1BVX-g9j4>1Bfpn%fKKKz`zgz<-^p2&c_4Eo5?aTu*NVjRG`U&&V2*PcgZp^NTe_@ zbU@`ne8Ch32GCh>t7I7%_Q^6Zuq7}v>|kPG0P(-dGB9|jFf)M8hlBC$Q<xb}Aj!L= zFf-hMng??a=nOfSdbSj11_5S>d8Kj;3~V{f;JZ6P^6NqT0%nF6Q2F<A3=EPP%nYDA z4MFlCzIX=cM0SXNd3go~&Jtz@4HgLBU7mr#X8|*V1rpz30W$;W96FGGki6>xW(E(a z{5p9C2Cg~G3>i>9NFJoW0Ey4FfSF+elz$hbehD)J=nfo^c`~4LI9D(;Y(SG|V1>8` zCLaLhF9NAw!^{9Ww+^KLKZw79nc)LeK0=X!L2?T-0|y&KKiGW-m>DcU_b-CdCMdCi z&e4X-S1B?u_#I(p@Ic}_9bsk&K;pX}VP;4`)1QGP&vk^EVFr{hqQt<!b%L2;13Se2 z91#BuGs6ZB2!DqX1B2uRW(LsxJD~J)Q;C5=sDpvw1P3HNg7};r3=9vTe5i97UO@G; zDKjv5e_>|$fW)`|!p!gkiSP1-nSp~7Vqc0f0|VO!W(EyTi1{${450EL^Vq&HGk8Gx zFO?Y>_<k@m1VH(`Dhv#Kf0!8((DY|O<@HrScgeCaR6zMK^&L>Yj|u~WFb5063@9Ha zzW~aI$*+L&VfOEU^3y^3Sy&iOK>0BF3s62x{sENVq{6@;%frI(0_xsXDhv!_YZw?l zK>5d17#J)!FfjZ;;yY|$VBp|F4Icq6Ncf4WGBBvdurO#q`KhW53|t8;3>HxJF!Moo zB7x!)X1)hh9>fQk9|7gV!XI>R5-9z`%&$N*A9R-z$h;0!1_p@}3=E+AltBE=stgQ5 zXBZe(K+QX^%D^Cdfq~%$l>Zhae}{qL0hG_G#=s!_fPq1S8{$8hdIKmQCT{`d!{kAC zGlA?=RbyaKe!;-t0hNcT4}kK6)EF2PpD-{)K>09v&{^{!{V?-Zpvi;oU`3V(-SY(E z!~7e;195M;8Uq9O4+e$?DF2We0|VzD28J0(e2G5{3>%>Q7a(~CMur<akoeP5XJF7t zVPVkVh47=*85p!OSQs*(e3<+WC?6(ozz0zei!ac9kf8Vi$s1*`Fo5opg7HD-1whqL zRA*o?D`8<+0Oem$XJ9ZaU;*DV1yawc!N6dc!@>Z%^AW@cnFrEe!H;Sm=>92?ypsk4 zgJ=Z{!wRVUYz+nmz8V&W14w+?8Wx5NQ2sHHd;<%^2YyKS!QB4?%I5^N0U{U~Ky3lg z-8&%pY)uA+R?r32HH-|PbIw3~aJ}8Z$N)O46T}DA%N9M5L*qbvkUkIwor?(KgXCFz zAoVtg52^<xZZI%_&XI%h1#dv^tpoAF_1Xmn1_34p1`z*@CIf@W2?hoUXgvt4Hw~bC zSiNb1#0S-zpn4q?P9S+uy$L!m6J#F9Z6FM)*J1LYdJ|MngZQO#ka`nz9xI3st2aUS zG=TV^dJ<G`79iE5pn9_c%7@jPE1-N>y|)9252`m0An`%<<_RbtTrbT*)YCBipu3Gh z_JjDKdQ$+cUId*D3zCP`iw0=&pnK+!<u5?xVf7~HOjwXStllg@t2aBKe6aftFf;66 zg~SgiEI}A_Ml47_tlm5URS)8W>dgyCd{Dg!y2lPVK3*WngX&EQw0hG3t==?%)@!hO z(}Dxyp3k7OkAE;QWI*M)wHO%KbXXWFpnQ;9K^S!Z9mu^FS_}*x1}qF6P<asF#(;%k z0utZRfQ12cM;=H$NG}M(>TQsEHUk!Ly$q5E*E23G@OpWc76StxsL=$ihrsok0}F!y zl)ncgZ^8nuhe7(m^_C3_g9TI`rr!a|hv|pa%P{@0dKsoa0;>Lj76SvX2MYtJUIvG^ zHUonu2P3#%hVgkh7#Su&)x+8a8#p213Gyol!|Gj7y8zVAH~>`-;)B{5uzDE82emV9 zpy`Lz!ytK3y8=|tg8U1sw?X#^g7~m@0H~e?yANbJ=#F}5JqT-8!0KI?dQiO!(hm+F z4i*MbJqzN)%m>x8AU-&JSXdYepyq+&i-&~)R?ouB1J$!2^)T}~pz2}fO@Q)Y=FNce zZL}E}1O-^Y^)g65h%Y9<!mxu25}pa#3=E16j0_i`{CV083_%`@;CdP4Ul8BTgOT9{ zlDxMEBf|$IelqAn8*YesAoU>q97udnKS%<~=hR_f;PYW*P(b1v`7kno>T!^L(BK8v z;~;*J4g-T>0wV*c9*6N=5*Qg0p!#9<WkC6Jbr={_Vi*`e_soFQgX`%81_sbwGa&vO z9R>!@90mqZy$|B^=`t|z&R}5J0oAXq%fKMr!oUE!pB*IcrOUv;Rl&e;0V?0B%fKMn z!@zI@%7@8=+6f@_%XAqSlqWDSe1OV>_=*!47=A$cF!c;Pknp;r%fO&Eg@Hi;iLWw+ zfk6Vwm(XKiknUh$&_Lo#b}%r2+94qOVCI3^9U#8B9s`3)0|P?`RDG5n1A|%(1H%F+ zA11#7%7@ACfbwDf2i;W(G7lCX0=y9S!sI2O{P}te44i8i7#yJd=XwkbtP>c)eRj~< zz##oM^cfi5=rb_zv_SgXFg|MwBZGng^gbU31_qG0ngQrQYsi_@AbySk0|RFa<P1|7 zUp$6^0i+DZmyTgz0G;g*;~U37&W?uhLGqwG4Pbo37zPH=+4V3!$b8V5wIKdh0|o}o z3<ib?Q1`qsU|>)Mr57k4+#aZ4U;v%n4pI+JUo{L2FBl;0A~8b-2GK7};Bf>{egpA2 zzc4X?&f*2}-3>u!zA`g_?mq$X8$o;)W`+-p5dC`%85krun86o+faF1ZF%D*M`vb%W z*XIJv46ydbTSEp0c?Tv2SbIath=GCAg$X=v08(#j#K6EB!UP^S0P)K~{0Jt70%-dL zCcgp7-w2WqV1kbWz~o{5ewcm<X#3zPNPP?w1E{MGGT+>ofq^xFi2>HW=rm?vP)cE9 zfVD3`e3=v`23Y%Ii!lR(ZUz&>4XAlA`#^Umg3JTSvt}?ca6sD!UyT_UWJ{PBVC@YM zU$}&cK>{i-W5U27TfxKtZ-1CDFbG#L!N(1pO&Azta+nxk?GX@PD2E9?PLKhTFJNK_ zfSL#53l%WI#|?T-7#PHAm>59g2B7#~X~Mt&>IcKd4Z!iSgo$AWR6jUARxmNFfbzlN zyM>A21eE{Wgn@x|2NMIR{Q>f?z9|C(>mDYC2hjFLxhVsK(g7xh7f5`W156Abp!|iV z3=FzQn7|ihfb_%6=YY0PK=Q0dm>4Xed{B7FUSMKyK;jEuU}Erq^1<PEg^3{mi7$MG zi6H{Y2Zfi+2_}XFB)-rICWZ<q9~53PXPCfu{({^C;tQQ&VweGy2Z!GcCWZy<ko*n{ zza3Edho%e+oEDIA15o&Cn=vp*Okn_z8-V!UW(*8MGZ?_*1|U92UT_KnxV-}6gUTBa z28|nl_?c!53@R%az~crW{!TMUc?D{pfcUq~7#I{cFff4n`5-={IRk_I76t}TKN-Y_ z$%E=w5FaKF>gR&^Iw19X7#L1)LHq-fSKq<_u1`VoAU?=EP(K&M4>4z8(Aoj1k3f8w zeo(&?#D}Sm;DMM2@(TzjK>5Aq3=AR%Amuwq{WEh02Hqo(@*T#PIRfctg7^X!3=F(? z7#Lna^?O<{FetuZVE6##XIn5ZNP`AZc_H?}<OTR3{4NUy1`!cP22j5eWZpfHc@m7^ zekF+i&4PhJ(14L40;(RyPk{0zEEyOCO&A$Ep!{G<1_n6?MurJUd>IEuh8a+PnI!{* zvI`@)9||)63P`^PBe)+5;=i|KU=a0TWOx8o597ap@<pu}7(@dY83gzt?$5PiV2}b` zoFM|?cUdto$fqzee2|3jH(4<-@MbVFa7aP;;C^QXBZGhxq&(KKW?<muVFcf621?H$ zJ|_<&g94JgAP*yh0TN$=hmpYoi7&##$l!p)2kG~K^1ZAf;TwR&7Z71&h(O{?h%ho_ zAo1lz7#RwX_|hVb3>8Rxkp2cJzW`*u0wY5Q5??@pkpXn)8OT2%c?ktZ2GG4{FuuG3 zBf|<L_0kHA44}KvVDcdS2cYt^K<4W(GMqr-3+OO1TtMSLfbzG1)SECeyg=d$m@qPY zK;tt=L&E<YNWBds0|ydcz=n}Q0FAEz<-Y)_cVT4EK;jFyFftgR@g1OiHX8;85g$ed z4<x>T4<kbW8b1NbR|TmLVPwca;tPZ@G8CZkLGv4+@NxmEk6~o!fXajT0x^sX6VUhz zp!{Tz`V>Zn9Y}nE6h?*vP(G+E0O1=@J~+J=FoNekK=!S$VPIftVPs&CfrQ^)8wLi+ z35*N^NPON2j0_S`{%0Eo2E{3i3>rv$`6-MH22j4XEdzt%3`PbAB)<F%Mg|WkKhc(f zL2V8rLjV$=cMc;1=>9Qe_jEwz`)nB)q|Y!i%s}Gvo?&EI0Odag*>{1FVFMCh{sJSz z4k%yVj)6h(3M0b_C_mhefkFBOBf||OKJN=gh6hmoB|8QN%{Pn;FOc{OZx|UqK=};z z3=Eo}#+NK4y!7lD7$i3^G6*2?c{eaJfaYI7{z<WCU{Kt`$e;n02l3^%FftfG`Ex+> zI~W-(pnOnVfp7qnf61PKL6(JyApwcc$HK&r0p+tgGB9XyFfkM$@fA3j7%HHAE08=7 z6GH=(4>B8sJD_}UdQf3vcy9&1696OyZdd=bVqoBzfoOk&_^ePqX#P!(fdSlRRbgOY zSO%Sc0L^WI_^+XSP&ZG9fq|g_G-<}bzyP|lF%ERU3*-(o2GCp+NFL-ikj0?6BoO~K z=<*-X{hdq<3?RNQ$TE<9ObiUy85kG}p?uIiq{w{Gm==f+a}TIZiOdJh9fA0-q545> zI*|EGK$l=JFo5TVKzx{ept%{)UAMEK6Oy3084&+Fln<Jl0r5jXmqbA4UO@U`=7Hv3 zKzx{a4Qvby_ds`#K`&YWwYiY_pf(SPp8%Ct;DD%y@j+uYAbD8$g62*@=Kq1J2hE*; z_%QoGV=Rx6!VA=f0r6qs1L`A#)cb)h<N^7g4-(!mK4>fzWHe~36m%~aOdlwoLHc0v z2I^aY_^@~b^({brm^(my2T-`f;tdqf$ov=3c!im7zzm6J7#}ot2~rP>w++k?cf!IM z6wk=^fyM?w@dArC(AXe|4~sX@*dT}xi#O2NA4osUJkZ!5h!1l=16sUwpz$TxQR4~J zRt3cqEZ#t4eIPz8-aun_AoF4I1{%8q@nQCX;uX|Cg!vyd)&}Ck!Vfey2I9lQ12nb; z(hoBaG`0oe!^{JXRe|CirXMs`1>(cZ2aWxJ_!iorTnL$S0qKXC2WpFg_%QQ8b0#1@ zES-S*<RCt%PYk*bZ3C#Y%>d~WgZR&(e9)yKpm>D2BZCnV&oF)kln-;)1~fjX*ahi_ z`5V-y1jQpPo<V&|5FZxLpmYJ^!{Qm#heYOs=4L_h!U0O;3=H5gVPrli9f0_-cyxfq zJB$zNbAjYx@e1l=q3}5v7(o74GlPUHsLc=J!~6?sQzP?1ZAef&z|@1<h#)@9{h&4= zh!2wowediFSUiB*a3DTR9@Ity@uxxV`C<UI!9aYNdQck+#D|#=YD0neF#Vu55{M7e z4{8H}_)L(yx)?xh91vdt$_KS!Kzw^BAJj$x@#CO;P#Xfohxs4WW&rVF@eQie|09)G zpgJ7HhslHLXwb#8F#Vu98N`RlgX&yRzJSSt>R1pTCJ(AJ*%%S^2dIt&@nQ0iSi(*+ zgD%3wBE`%Ax~T?L;tu!@8x{u8+!V5mJa~YM1w7V(EdCR!9#qaCi=Tyx?*L6+AxVJl zTxDWl0{1yU!k~MSKs2aa1>Fn=nj-}9Vb~68FK8|iCI+HIL3i^q@GuC1IM6$hKuplw z704XWTpNfF!=O8S7#VmNpvf70zYk1~8Qt*=3=Gv^d)XL3cL2iFf#^`EIOx7U5Fds) zpyHshL6{hb2F;y;X5&F~Bp^NvZwH4D8v|%g9wr8&XF<g`K-(c8F%W(aH6N4@L3|iC zgNj>#F0X<KGB7ZtK*d3KiX#bwnatoBMKBXifbKH|#S2UTl%GNDg<x}-7{CjhKmt(Q z1QiGMC80uK3RLcaT4SI&FR%cVcn(z$T7v-<0#nDp{)LP&fd!xhsB8hb2ei%!Bm^;+ zfdMpj2QvQwTDp=0-+TobYXPZ;W2n6kKo>p11tDXmp!5(96$7OY5OX2aJ)k~3h!4Zx znLzhE@i2hyfC7nu?tuc)E1>3s`rsfw41?AZfy@Wp69p181rbop464MSY#22QY7VHJ zgo%OZ7^pa?PXyw_upd+$RBpn=K=dc5`*%ozcnGWqRS)V*Aw(G%N}%GPJ`A#W7gQY7 z_dphZ4K*Ls=Ry|W2Negk&5^}dLd6-N?P8dm8q|DHUjZftqVGe+L47C?ABM%C@eAr} zz{EiG6{z{J_8dqIhM7SV4=@gt&SwPOIfy9tIH2wOO3=DF27d4wIgnD=-D{w`mjxMk z;3Xy4ItB)YE(VY}pj(SUsu>tOp!rV^Y(64!LYvzRptX4*b0DfgB@~2}1GN{zgUkhh z*r5BXg^<z-%$z5TpgVMVKufVe%HTc>28%--0lGLCEX%;a#|%;ry44aQ0CU-OCWttU z52GhR#i79hFDY`M;;?)IQ||^9hq(_X4$}_`FHjW<8lwcc0K^B)Ux3E=kollF92nn{ z0Wzis<Acg<&=?(z57G}>>jC3~)Pu(0V0@5z&=?zxZwu<TL4yEfKS(`j3=Jj^QeVIc z;(_m<1~v5<Ky#oV5s-S2eW0~L$b8V4B{Cm0Ck5k!%!k!iApIcoL2E}~@*wj-Ye!&w zkb2Ns5f~q&UIAMEgD8+0pgBL7JV-xiP70Y13wM}$bp8bB1>c~Q54sX?4iX=9z1s>T zKIn?KEl7M9P=6MQ?+Th9LgItM_X!do<i0;pJ}BFQ;-A9^<XXsDR*-*1kofkXb*@N! zkpFei_!ek<7c_nV8b1Td2l*FtZ!xHi4{`yB4=Q3nZi4YaxdwE{CyWoeuQLY3f#xGn z_=DE+f`mZ&L3~iYMdsIlI8gQK(EbBR2&5ilALw3WWc~&a2dW+<54tNEBm`3ryVD)U z2hG7D%Y*v0$b4|3MKTYxb{AP5)bB;+gVydM^FeEOk@=vtyU2Xdure|qw3Zi{-v9~} zB>O>ge#r6*(BxtDIm~|t(Bxt5LYVvmG<i^e3fX*E6Bni)(hh`_v!L(=-5ZUp9@Otg z=7ZKOA@f0NmXP_Nwa3VO(6|6HA2c3>%%1?V6e&DFYmbrTA?+B1`#|jlWcdqd`ax@r zk>x+2$%EERA<Ki-A|vw^Kp7FqJ)m(4WO>kd6f!>mO+9EF16dw4UWLqWKvO>fjSp)_ z!{P@t4uY&7G-ry;zksG6G)Iao4_cFj%!gIbF!MokrpWT3IZ|XkX#5PB4_Zr&%m>Yh zBJ*MQe8bEK&50t*!`izrdC(jwvOH*QH8LO6e@Etn?h8legT{f7`42z_BgNkbG(N0B z01^fz4FTu{;V?dE90}Px12lQiUF68}pz%Xwegc|$&^Q#bJgh$ivu^^LdeHbIvic2Z z@}O}nWO>k<HDo?$&KQ{w8oxy5!@6)V`vss)GZ<e1jc<U)2aS&)n+IBJj?4$GHAm(b zpy>yVk0Q%YK$Bm9#@~R(KY+%+fW`-n*C3k@8m~d-LvLgSX9IBjgKwTilrIWse9)Rc zWc{Eya%4VeP8^vJngd7X7oeHffX1JI#s`fnBI}3s17Y!V08RY`H2woL{s%NZtf2}s zPXO8gg7HD)(a83L#x;@o9!Tmz?STk1K4?uNOg%^*w5Ac64;lwW=FdRWzXFX98y5k& z4`d!_uK}|D8))i5<EqH=puGjie9+zkWWEHnJqt4*wATPx9<;_0nGYIoN9KdZX_5J$ zHH^r7(B1-MK5QHVW*=y80kS-3F99+iH13Pc2d!a5=7ZKSBJ)9e2ax%oabaXWXs-Y= zUjoz+hL)G0@CB_|M3x7wSw!Z8)+{3PL2DL~`59>Dfz~V{%Y)V^BJ)Ax(#ZT3X!=2G z5|QO$<2tbL1MM9^mItj#MCSiMGY_-|5m_Fz1`(ML+G~K!2kk9D=7Yw?k@=v#1IT<> z{{!Z}3T}`rq<jH|FK7)SNCYN715F;Z<^x$Cv<4BG4_bqW%m=MGMCQLhGY_=J5LuoB zI=%z5AGD?rSst{e5Sed*rrraM59{B-%*#NNuR!C&`mZqcpuG^t_JP(QBJ)9O5Rv&O z(9DC4gTl-Mt!Y74{{u}u2PhJd(mSkw2-6SRYk{mEHeLvmhmBvr_z`I4f%aw~>#snQ z??B_vK;y4K<Ae5gAe(mrP5uTN{{<TV2O6IP+P{anUjmJ<fyM`|@j<rV15F;Z#s^tG z15LgHjSm|ygxL=pe}M5<psC-1#s}>^LALJ(nmla00%qP1G<gp24fKfoD1pY;K;v7W z@jcM^5or7jG(Ko=46=JW(Bx;J@nPc~F!%33lRtsRzk$YofyRdo)Pc+c_16WU15Pl$ z1`;3Cb+ka^d!X?n(D)f>{0cOF2O56{8h-^Ee+L@>1RDPa8vg|v{|6eM13K{m@-N7L z5@>u4G`<BI-vf;wfyU23<5!^ZJJ9$u(D*CR_&d<}C(!se(D*OV_&?D2973Sf2TAW> z|D*9W(D)W;d=E5!1R6gBjbDMr??B_vK;y4K<L^M@pFrc^K;yqa<NrY8a|omPAC0ep z#<xJ@d!X?n(D)f>{0cOF2O56{8h-^Ee+L@>1RDPa8vg|v{|6eMLj=wLXnYMcz6BcJ z1C1Yn#?L_GSD^7b(D*aZ_$$!(JJ9$i(D*md_%G1-KhXFbqG<j{<7=StEztNLX#5B? zeg+!90*&8+#-D-4UxCKofyO_9#=n8ae}TsTfyU>6PQby-KQz7u8s7qq?}5gTK;vhi z@hi~y9ccU+X#5pu{2gfg6KMP!X#5vw{2yq14p5~BZSR50KQz7u8s7qq?}5gTK;vhi z@hi~y9ccU+X#5pu{2gfg6KMP!X#5vw{2yq14(LP_JpR%68fbhAG`<HKKLU-PfyS>u z<9DF(XQ1&{pz(L0@lT-fZ=msCpz(j8@i`>X@;@411C4Ki#`i$uN1*XD(D)T-{0=n! z3^e`<H2w}W{s}bx4K)4>H2x1XK5X3qJpQ5c-7vle8XvSq9TZ|PdDuKVj1O9ajw}ya zpM=Z@tvN^LgVvZM^Jk#h2U=5(EDxKHhuH^OLyjze15H0@%{a3B4>Wnu8gXQK326`y z(H;V=2?vS5?6W|V2dx1|mXAP_2d()=mItkmg7LxgTS)3b{542?(D>sXH2xVh{v9;_ z8#MkOG(Hb>-7UyJAoFF=_&R8O8#KNT8b1b&pM%D)LF4zJ@#moN*P!wDpz+V3@$aDV z-=OjTpz(R2>nGs;N8{_D@omufK4|<HG=2^mzXpvz1&I&pdM`oaZ$aZ9LE~RR<3B;; ze?jB3K-W2g{0p*A1dXqP#y3IZyP)wy(D*56{1P;N3mSh48h;5Ie+wG_2pazi8vh9z z{|g$Q1-h;q?te7C3L4)8jqifS4?*Lnpz%x4_$_GsDQJAydSjUT_8`fF+9PMs_;=9w zZ_xOE(D*#i1tlQ!LFR$ZR)Cd1AifToybT)P2aO+t#?L|H*P!ux(D-xE_-oMkd(il2 z(D--I_;1kof6(|m&~^E6|D*AB(D*iJd>=G^3>rTNjbDSt??L0wLF2DM<L^P^pF!i_ zLF2zc<Nrb9^FY@%!~KuO*Fodkpz(ds_%UdF&>2w3>B-K~$<^7x-Nl}pS6Nk^L9Zw^ z2XcJ@nB^4WrU$yqpd`6~{8R8CXXV0-fFF*Bd@wHq{1|5FC14B?O`yZ+phr)GomP?@ z54~>!#)Y0ti*$A#{J>DC8L(5EQI6e1IVTl<z@T1EPFXJaGV9{ZbchY0i!X3pP6Txd z@>#hM=OBx~k0%5LDA-pJ<BGGB^t027Qd7Z~sOuG%6qO_<G3XUn=7MSH0e~P)dQKs3 z3_eayhVhml$~fK-Oq<19K)7Ja5W+KyHw1Hy<IO>m1`x_D-Vn+)G>09V=;P#M7H<Yp zWD21SAe3>uAz0K9N*h3F!+2AWVgm?e7H<gVnnGzqC~W|uAqO#<fRvlYgDFD@Wg2e? z<r<nWKvWrnRGGztDMJWl9B%~Xn#CK!j%PFisWOcRQ^s%#$~6Sj2Jwc_qaHC&R)kn( z2r|Sh-VjV1Luo@04L({CT^sBGNdu@hFl`)f2-_F|%0=YkN=C?<Fmw~zpbMJRpzA<j z@}ROG#s<-#WdtBG=yW+q2#i5%zCer#&~^og4Z_fARS*-5L3%+`r7-niLC|`^+YlCP zofc^L0o3k<>4y%-GJw`afy_X+{{qxr*!oh4X$-JwY8aOxhLM2*oBb7cAWA@IGk{ni z`$1(Py8VKX<DX#5A3#dM_=YZ+g0D-3@G$M?#bG}OXhApw<m?HMUXcADKcd?YTGxin z{|eB38_;SEWCR$)^n*DJ3=DG^u>0QvYCkNTLH2|6f-p!gOng0f9{^~DJxCOR9f-7l zH4ghdu-Ffpaz(el6B_=Yw1Kdlfgu5E|1M-f=y(mXPzY2%S~6#lf-X=5#W}KiP*@}L zcY^lhF<|B)*uECnI%b$2nEPS;ebDejw;!e-w3ZX57DS`#p9*z9NDdiCK;55MhAax{ zpCXG)#F73Ypy3ZK1{gr`4~lb`{UA2T9MH57dYFL3_iO@@3=B2UdLP2Wv>!CBj4l5~ zfHp&c)}=!AgX{+l!K2#`+T+9Q^B=S=0JMb+W(`9H)Zx%+I=DExTN@ccyBXlpaE8TI U2otuB2&B&(8rL8h=x`7N0D<kP;s5{u literal 27280 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 diff --git a/pkg/ebpf/tracer.go b/pkg/ebpf/tracer.go index 2253c1f4b..29eba8bf1 100644 --- a/pkg/ebpf/tracer.go +++ b/pkg/ebpf/tracer.go @@ -8,6 +8,7 @@ import ( "github.com/cilium/ebpf" "github.com/cilium/ebpf/btf" + "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/ringbuf" "github.com/cilium/ebpf/rlimit" "github.com/netobserv/netobserv-ebpf-agent/pkg/ifaces" @@ -17,7 +18,7 @@ import ( ) // $BPF_CLANG and $BPF_CFLAGS are set by the Makefile. -//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -type flow_metrics_t -type flow_id_t -type flow_record_t Bpf ../../bpf/flows.c -- -I../../bpf/headers +//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -type flow_metrics_t -type flow_id_t -type flow_record_t -type tcp_drops_t -type dns_record_t Bpf ../../bpf/flows.c -- -I../../bpf/headers const ( qdiscType = "clsact" @@ -34,20 +35,22 @@ var log = logrus.WithField("component", "ebpf.FlowFetcher") // and to flows that are forwarded by the kernel via ringbuffer because could not be aggregated // in the map type FlowFetcher struct { - objects *BpfObjects - qdiscs map[ifaces.Interface]*netlink.GenericQdisc - egressFilters map[ifaces.Interface]*netlink.BpfFilter - ingressFilters map[ifaces.Interface]*netlink.BpfFilter - ringbufReader *ringbuf.Reader - cacheMaxSize int - enableIngress bool - enableEgress bool + objects *BpfObjects + qdiscs map[ifaces.Interface]*netlink.GenericQdisc + egressFilters map[ifaces.Interface]*netlink.BpfFilter + ingressFilters map[ifaces.Interface]*netlink.BpfFilter + ringbufReader *ringbuf.Reader + cacheMaxSize int + enableIngress bool + enableEgress bool + tcpDropsTracePoint link.Link + dnsTrackerTracePoint link.Link } func NewFlowFetcher( traceMessages bool, sampling, cacheMaxSize int, - ingress, egress bool, + ingress, egress, tcpDrops, dnsTracker bool, ) (*FlowFetcher, error) { if err := rlimit.RemoveMemlock(); err != nil { log.WithError(err). @@ -73,6 +76,7 @@ func NewFlowFetcher( }); err != nil { return nil, fmt.Errorf("rewriting BPF constants definition: %w", err) } + if err := spec.LoadAndAssign(&objects, nil); err != nil { var ve *ebpf.VerifierError if errors.As(err, &ve) { @@ -88,20 +92,39 @@ func NewFlowFetcher( * for more details. */ btf.FlushKernelSpec() + + var tcpDropsLink link.Link + if tcpDrops { + tcpDropsLink, err = link.Tracepoint("skb", "kfree_skb", objects.KfreeSkb, nil) + if err != nil { + return nil, fmt.Errorf("failed to attach the BPF program to kfree_skb tracepoint: %w", err) + } + } + + var dnsTrackerLink link.Link + if dnsTracker { + dnsTrackerLink, err = link.Tracepoint("net", "net_dev_queue", objects.TraceNetPackets, nil) + if err != nil { + return nil, fmt.Errorf("failed to attach the BPF program to trace_net_packets: %w", err) + } + } + // read events from igress+egress ringbuffer flows, err := ringbuf.NewReader(objects.DirectFlows) if err != nil { return nil, fmt.Errorf("accessing to ringbuffer: %w", err) } return &FlowFetcher{ - objects: &objects, - ringbufReader: flows, - egressFilters: map[ifaces.Interface]*netlink.BpfFilter{}, - ingressFilters: map[ifaces.Interface]*netlink.BpfFilter{}, - qdiscs: map[ifaces.Interface]*netlink.GenericQdisc{}, - cacheMaxSize: cacheMaxSize, - enableIngress: ingress, - enableEgress: egress, + objects: &objects, + ringbufReader: flows, + egressFilters: map[ifaces.Interface]*netlink.BpfFilter{}, + ingressFilters: map[ifaces.Interface]*netlink.BpfFilter{}, + qdiscs: map[ifaces.Interface]*netlink.GenericQdisc{}, + cacheMaxSize: cacheMaxSize, + enableIngress: ingress, + enableEgress: egress, + tcpDropsTracePoint: tcpDropsLink, + dnsTrackerTracePoint: dnsTrackerLink, }, nil } @@ -217,6 +240,14 @@ func (m *FlowFetcher) Close() error { log.Debug("unregistering eBPF objects") var errs []error + + if m.tcpDropsTracePoint != nil { + m.tcpDropsTracePoint.Close() + } + + if m.dnsTrackerTracePoint != nil { + m.dnsTrackerTracePoint.Close() + } // m.ringbufReader.Read is a blocking operation, so we need to close the ring buffer // from another goroutine to avoid the system not being able to exit if there // isn't traffic in a given interface diff --git a/pkg/exporter/kafka_proto_test.go b/pkg/exporter/kafka_proto_test.go index d07028c80..9bdb0b2b8 100644 --- a/pkg/exporter/kafka_proto_test.go +++ b/pkg/exporter/kafka_proto_test.go @@ -65,7 +65,7 @@ func TestProtoConversion(t *testing.T) { assert.EqualValues(t, 4321, r.Transport.SrcPort) assert.EqualValues(t, 1234, r.Transport.DstPort) assert.EqualValues(t, 210, r.Transport.Protocol) - assert.EqualValues(t, 8, r.Icmp.IcmpType) + assert.EqualValues(t, 8, r.IcmpType) assert.Equal(t, record.TimeFlowStart.UnixMilli(), r.TimeFlowStart.AsTime().UnixMilli()) assert.Equal(t, record.TimeFlowEnd.UnixMilli(), r.TimeFlowEnd.AsTime().UnixMilli()) assert.EqualValues(t, 789, r.Bytes) diff --git a/pkg/exporter/proto.go b/pkg/exporter/proto.go index 94f9bcea0..df460b1ba 100644 --- a/pkg/exporter/proto.go +++ b/pkg/exporter/proto.go @@ -38,7 +38,7 @@ func flowToPB(record *flow.Record) *pbflow.Record { } func v4FlowToPB(fr *flow.Record) *pbflow.Record { - return &pbflow.Record{ + var pbflowRecord = pbflow.Record{ EthProtocol: uint32(fr.Id.EthProtocol), Direction: pbflow.Direction(fr.Id.Direction), DataLink: &pbflow.DataLink{ @@ -54,11 +54,9 @@ func v4FlowToPB(fr *flow.Record) *pbflow.Record { SrcPort: uint32(fr.Id.SrcPort), DstPort: uint32(fr.Id.DstPort), }, - Icmp: &pbflow.Icmp{ - IcmpType: uint32(fr.Id.IcmpType), - IcmpCode: uint32(fr.Id.IcmpCode), - }, - Bytes: fr.Metrics.Bytes, + IcmpType: uint32(fr.Id.IcmpType), + IcmpCode: uint32(fr.Id.IcmpCode), + Bytes: fr.Metrics.Bytes, TimeFlowStart: ×tamppb.Timestamp{ Seconds: fr.TimeFlowStart.Unix(), Nanos: int32(fr.TimeFlowStart.Nanosecond()), @@ -67,16 +65,36 @@ func v4FlowToPB(fr *flow.Record) *pbflow.Record { Seconds: fr.TimeFlowEnd.Unix(), Nanos: int32(fr.TimeFlowEnd.Nanosecond()), }, - Packets: uint64(fr.Metrics.Packets), - Duplicate: fr.Duplicate, - AgentIp: agentIP(fr.AgentIP), - Flags: uint32(fr.Metrics.Flags), - Interface: string(fr.Interface), + Packets: uint64(fr.Metrics.Packets), + Duplicate: fr.Duplicate, + AgentIp: agentIP(fr.AgentIP), + Flags: uint32(fr.Metrics.Flags), + Interface: string(fr.Interface), + TcpDropBytes: fr.Metrics.TcpDrops.Bytes, + TcpDropPackets: uint64(fr.Metrics.TcpDrops.Packets), + TcpDropLatestFlags: uint32(fr.Metrics.TcpDrops.LatestFlags), + TcpDropLatestState: uint32(fr.Metrics.TcpDrops.LatestState), + TcpDropLatestDropCause: fr.Metrics.TcpDrops.LatestDropCause, + DnsId: uint32(fr.Metrics.DnsRecord.Id), + DnsFlags: uint32(fr.Metrics.DnsRecord.Flags), + } + if fr.Metrics.DnsRecord.ReqMonoTimeTs != 0 { + pbflowRecord.TimeDnsReq = ×tamppb.Timestamp{ + Seconds: fr.TimeDNSRequest.Unix(), + Nanos: int32(fr.TimeDNSRequest.Nanosecond()), + } + } + if fr.Metrics.DnsRecord.RspMonoTimeTs != 0 { + pbflowRecord.TimeDnsRsp = ×tamppb.Timestamp{ + Seconds: fr.TimeDNSResponse.Unix(), + Nanos: int32(fr.TimeDNSResponse.Nanosecond()), + } } + return &pbflowRecord } func v6FlowToPB(fr *flow.Record) *pbflow.Record { - return &pbflow.Record{ + var pbflowRecord = pbflow.Record{ EthProtocol: uint32(fr.Id.EthProtocol), Direction: pbflow.Direction(fr.Id.Direction), DataLink: &pbflow.DataLink{ @@ -92,11 +110,9 @@ func v6FlowToPB(fr *flow.Record) *pbflow.Record { SrcPort: uint32(fr.Id.SrcPort), DstPort: uint32(fr.Id.DstPort), }, - Icmp: &pbflow.Icmp{ - IcmpType: uint32(fr.Id.IcmpType), - IcmpCode: uint32(fr.Id.IcmpCode), - }, - Bytes: fr.Metrics.Bytes, + IcmpType: uint32(fr.Id.IcmpType), + IcmpCode: uint32(fr.Id.IcmpCode), + Bytes: fr.Metrics.Bytes, TimeFlowStart: ×tamppb.Timestamp{ Seconds: fr.TimeFlowStart.Unix(), Nanos: int32(fr.TimeFlowStart.Nanosecond()), @@ -105,12 +121,32 @@ func v6FlowToPB(fr *flow.Record) *pbflow.Record { Seconds: fr.TimeFlowEnd.Unix(), Nanos: int32(fr.TimeFlowEnd.Nanosecond()), }, - Packets: uint64(fr.Metrics.Packets), - Flags: uint32(fr.Metrics.Flags), - Interface: fr.Interface, - Duplicate: fr.Duplicate, - AgentIp: agentIP(fr.AgentIP), + Packets: uint64(fr.Metrics.Packets), + Flags: uint32(fr.Metrics.Flags), + Interface: fr.Interface, + Duplicate: fr.Duplicate, + AgentIp: agentIP(fr.AgentIP), + TcpDropBytes: fr.Metrics.TcpDrops.Bytes, + TcpDropPackets: uint64(fr.Metrics.TcpDrops.Packets), + TcpDropLatestFlags: uint32(fr.Metrics.TcpDrops.LatestFlags), + TcpDropLatestState: uint32(fr.Metrics.TcpDrops.LatestState), + TcpDropLatestDropCause: fr.Metrics.TcpDrops.LatestDropCause, + DnsId: uint32(fr.Metrics.DnsRecord.Id), + DnsFlags: uint32(fr.Metrics.DnsRecord.Flags), + } + if fr.Metrics.DnsRecord.ReqMonoTimeTs != 0 { + pbflowRecord.TimeDnsReq = ×tamppb.Timestamp{ + Seconds: fr.TimeDNSRequest.Unix(), + Nanos: int32(fr.TimeDNSRequest.Nanosecond()), + } + } + if fr.Metrics.DnsRecord.RspMonoTimeTs != 0 { + pbflowRecord.TimeDnsRsp = ×tamppb.Timestamp{ + Seconds: fr.TimeDNSResponse.Unix(), + Nanos: int32(fr.TimeDNSResponse.Nanosecond()), + } } + return &pbflowRecord } // Mac bytes are encoded in the same order as in the array. This is, a Mac diff --git a/pkg/flow/account.go b/pkg/flow/account.go index a38b8140a..b88840e04 100644 --- a/pkg/flow/account.go +++ b/pkg/flow/account.go @@ -87,7 +87,7 @@ func (c *Accounter) evict(entries map[ebpf.BpfFlowId]*ebpf.BpfFlowMetrics, evict monotonicNow := uint64(c.monoClock()) records := make([]*Record, 0, len(entries)) for key, metrics := range entries { - records = append(records, NewRecord(key, *metrics, now, monotonicNow)) + records = append(records, NewRecord(key, metrics, now, monotonicNow)) } alog.WithField("numEntries", len(records)).Debug("records evicted from userspace accounter") evictor <- records diff --git a/pkg/flow/account_test.go b/pkg/flow/account_test.go index c53df299c..211348fa7 100644 --- a/pkg/flow/account_test.go +++ b/pkg/flow/account_test.go @@ -64,18 +64,30 @@ func TestEvict_MaxEntries(t *testing.T) { Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 123, Packets: 1, StartMonoTimeTs: 123, EndMonoTimeTs: 123, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 123, + RspMonoTimeTs: 0, + }, }, } inputs <- &RawRecord{ Id: k2, Metrics: ebpf.BpfFlowMetrics{ Bytes: 456, Packets: 1, StartMonoTimeTs: 456, EndMonoTimeTs: 456, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 456, + RspMonoTimeTs: 0, + }, }, } inputs <- &RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 321, Packets: 1, StartMonoTimeTs: 789, EndMonoTimeTs: 789, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 789, + RspMonoTimeTs: 789, + }, }, } requireNoEviction(t, evictor) @@ -85,6 +97,10 @@ func TestEvict_MaxEntries(t *testing.T) { Id: k3, Metrics: ebpf.BpfFlowMetrics{ Bytes: 111, Packets: 1, StartMonoTimeTs: 888, EndMonoTimeTs: 888, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 888, + RspMonoTimeTs: 888, + }, }, } @@ -105,20 +121,30 @@ func TestEvict_MaxEntries(t *testing.T) { Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 123, Packets: 1, StartMonoTimeTs: 123, EndMonoTimeTs: 123, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 123, + RspMonoTimeTs: 0, + }, }, }, - TimeFlowStart: now.Add(-(1000 - 123) * time.Nanosecond), - TimeFlowEnd: now.Add(-(1000 - 123) * time.Nanosecond), + TimeFlowStart: now.Add(-(1000 - 123) * time.Nanosecond), + TimeFlowEnd: now.Add(-(1000 - 123) * time.Nanosecond), + TimeDNSRequest: now.Add(-(1000 - 123) * time.Nanosecond), }, k2: { RawRecord: RawRecord{ Id: k2, Metrics: ebpf.BpfFlowMetrics{ Bytes: 456, Packets: 1, StartMonoTimeTs: 456, EndMonoTimeTs: 456, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 456, + RspMonoTimeTs: 0, + }, }, }, - TimeFlowStart: now.Add(-(1000 - 456) * time.Nanosecond), - TimeFlowEnd: now.Add(-(1000 - 456) * time.Nanosecond), + TimeFlowStart: now.Add(-(1000 - 456) * time.Nanosecond), + TimeFlowEnd: now.Add(-(1000 - 456) * time.Nanosecond), + TimeDNSRequest: now.Add(-(1000 - 456) * time.Nanosecond), }, }, received) } @@ -141,18 +167,30 @@ func TestEvict_Period(t *testing.T) { Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 10, Packets: 1, StartMonoTimeTs: 123, EndMonoTimeTs: 123, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 123, + RspMonoTimeTs: 0, + }, }, } inputs <- &RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 10, Packets: 1, StartMonoTimeTs: 456, EndMonoTimeTs: 456, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 456, + RspMonoTimeTs: 0, + }, }, } inputs <- &RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 10, Packets: 1, StartMonoTimeTs: 789, EndMonoTimeTs: 789, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 789, + RspMonoTimeTs: 0, + }, }, } // Forcing at least one eviction here @@ -161,12 +199,20 @@ func TestEvict_Period(t *testing.T) { Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 10, Packets: 1, StartMonoTimeTs: 1123, EndMonoTimeTs: 1123, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 1123, + RspMonoTimeTs: 0, + }, }, } inputs <- &RawRecord{ Id: k1, Metrics: ebpf.BpfFlowMetrics{ Bytes: 10, Packets: 1, StartMonoTimeTs: 1456, EndMonoTimeTs: 1456, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 1456, + RspMonoTimeTs: 0, + }, }, } @@ -183,10 +229,15 @@ func TestEvict_Period(t *testing.T) { StartMonoTimeTs: 123, EndMonoTimeTs: 123, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 123, + RspMonoTimeTs: 0, + }, }, }, - TimeFlowStart: now.Add(-1000 + 123), - TimeFlowEnd: now.Add(-1000 + 123), + TimeFlowStart: now.Add(-1000 + 123), + TimeFlowEnd: now.Add(-1000 + 123), + TimeDNSRequest: now.Add(-1000 + 123), }, *records[0]) records = receiveTimeout(t, evictor) require.Len(t, records, 1) @@ -199,10 +250,15 @@ func TestEvict_Period(t *testing.T) { StartMonoTimeTs: 1123, EndMonoTimeTs: 1123, Flags: 1, + DnsRecord: ebpf.BpfDnsRecordT{ + ReqMonoTimeTs: 1123, + RspMonoTimeTs: 0, + }, }, }, - TimeFlowStart: now.Add(-1000 + 1123), - TimeFlowEnd: now.Add(-1000 + 1123), + TimeFlowStart: now.Add(-1000 + 1123), + TimeFlowEnd: now.Add(-1000 + 1123), + TimeDNSRequest: now.Add(-1000 + 1123), }, *records[0]) // no more flows are evicted diff --git a/pkg/flow/record.go b/pkg/flow/record.go index 60dc0148a..d8313b1cd 100644 --- a/pkg/flow/record.go +++ b/pkg/flow/record.go @@ -37,9 +37,11 @@ type RawRecord ebpf.BpfFlowRecordT type Record struct { RawRecord // TODO: redundant field from RecordMetrics. Reorganize structs - TimeFlowStart time.Time - TimeFlowEnd time.Time - Interface string + TimeFlowStart time.Time + TimeFlowEnd time.Time + TimeDNSRequest time.Time + TimeDNSResponse time.Time + Interface string // Duplicate tells whether this flow has another duplicate so it has to be excluded from // any metrics' aggregation (e.g. bytes/second rates between two pods). // The reason for this field is that the same flow can be observed from multiple interfaces, @@ -54,20 +56,30 @@ type Record struct { func NewRecord( key ebpf.BpfFlowId, - metrics ebpf.BpfFlowMetrics, + metrics *ebpf.BpfFlowMetrics, currentTime time.Time, monotonicCurrentTime uint64, ) *Record { startDelta := time.Duration(monotonicCurrentTime - metrics.StartMonoTimeTs) endDelta := time.Duration(monotonicCurrentTime - metrics.EndMonoTimeTs) - return &Record{ + var reqDNS, rspDNS time.Duration + var record = Record{ RawRecord: RawRecord{ Id: key, - Metrics: metrics, + Metrics: *metrics, }, TimeFlowStart: currentTime.Add(-startDelta), TimeFlowEnd: currentTime.Add(-endDelta), } + if metrics.DnsRecord.ReqMonoTimeTs != 0 { + reqDNS = time.Duration(monotonicCurrentTime - metrics.DnsRecord.ReqMonoTimeTs) + record.TimeDNSRequest = currentTime.Add(-reqDNS) + } + if metrics.DnsRecord.RspMonoTimeTs != 0 { + rspDNS = time.Duration(monotonicCurrentTime - metrics.DnsRecord.RspMonoTimeTs) + record.TimeDNSResponse = currentTime.Add(-rspDNS) + } + return &record } // IP returns the net.IP equivalent object diff --git a/pkg/flow/record_test.go b/pkg/flow/record_test.go index 8f1fc59b0..fc6b0abed 100644 --- a/pkg/flow/record_test.go +++ b/pkg/flow/record_test.go @@ -32,7 +32,17 @@ func TestRecordBinaryEncoding(t *testing.T) { 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, // u64 flow_end_time 0x13, 0x14, //flags 0x33, // u8 errno - + // tcp_drops structure + 0x10, 0x11, 0x12, 0x13, // u32 packets + 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, // u64 bytes + 0x1c, 0x1d, //flags + 0x1e, // state + 0x11, 0, 0, 0, //case + // dns_record structure + 01, 00, // id + 0x80, 00, // flags + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // req ts + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, // rsp ts })) require.NoError(t, err) @@ -58,6 +68,19 @@ func TestRecordBinaryEncoding(t *testing.T) { EndMonoTimeTs: 0x1a19181716151413, Flags: 0x1413, Errno: 0x33, + TcpDrops: ebpf.BpfTcpDropsT{ + Packets: 0x13121110, + Bytes: 0x1b1a191817161514, + LatestFlags: 0x1d1c, + LatestState: 0x1e, + LatestDropCause: 0x11, + }, + DnsRecord: ebpf.BpfDnsRecordT{ + Id: 0x0001, + Flags: 0x0080, + ReqMonoTimeTs: 0x1817161514131211, + RspMonoTimeTs: 0x2827262524232221, + }, }, }, *fr) // assert that IP addresses are interpreted as IPv4 addresses diff --git a/pkg/flow/tracer_map.go b/pkg/flow/tracer_map.go index e067e34e0..8a0118893 100644 --- a/pkg/flow/tracer_map.go +++ b/pkg/flow/tracer_map.go @@ -104,7 +104,7 @@ func (m *MapTracer) evictFlows(ctx context.Context, enableGC bool, forwardFlows } forwardingFlows = append(forwardingFlows, NewRecord( flowKey, - *aggregatedMetrics, + aggregatedMetrics, currentTime, uint64(monotonicTimeNow), )) diff --git a/pkg/grpc/grpc_test.go b/pkg/grpc/grpc_test.go index efdd355bf..4328dc174 100644 --- a/pkg/grpc/grpc_test.go +++ b/pkg/grpc/grpc_test.go @@ -139,9 +139,10 @@ func BenchmarkIPv4GRPCCommunication(b *testing.B) { client := cc.Client() f := &pbflow.Record{ - EthProtocol: 2048, - Bytes: 456, - Flags: 1, + EthProtocol: 2048, + Bytes: 456, + Flags: 1, + Direction: pbflow.Direction_EGRESS, TimeFlowStart: timestamppb.Now(), TimeFlowEnd: timestamppb.Now(), @@ -162,10 +163,15 @@ func BenchmarkIPv4GRPCCommunication(b *testing.B) { SrcPort: 23000, DstPort: 443, }, - Icmp: &pbflow.Icmp{ - IcmpType: 8, - IcmpCode: 10, - }, + IcmpType: 8, + IcmpCode: 10, + TcpDropBytes: 100, + TcpDropPackets: 1, + TcpDropLatestFlags: 1, + TcpDropLatestState: 2, + TcpDropLatestDropCause: 3, + TimeDnsReq: timestamppb.Now(), + TimeDnsRsp: timestamppb.Now(), } records := &pbflow.Records{} for i := 0; i < 100; i++ { @@ -215,10 +221,17 @@ func BenchmarkIPv6GRPCCommunication(b *testing.B) { SrcPort: 23000, DstPort: 443, }, - Icmp: &pbflow.Icmp{ - IcmpType: 8, - IcmpCode: 10, - }, + IcmpType: 8, + IcmpCode: 10, + TcpDropBytes: 100, + TcpDropPackets: 1, + TcpDropLatestFlags: 1, + TcpDropLatestState: 2, + TcpDropLatestDropCause: 3, + DnsId: 1, + DnsFlags: 100, + TimeDnsReq: timestamppb.Now(), + TimeDnsRsp: timestamppb.Now(), } records := &pbflow.Records{} for i := 0; i < 100; i++ { diff --git a/pkg/pbflow/flow.pb.go b/pkg/pbflow/flow.pb.go index af4bee082..60076f902 100644 --- a/pkg/pbflow/flow.pb.go +++ b/pkg/pbflow/flow.pb.go @@ -177,9 +177,19 @@ type Record struct { // From all the duplicate flows, one will set this value to false and the rest will be true. Duplicate bool `protobuf:"varint,11,opt,name=duplicate,proto3" json:"duplicate,omitempty"` // Agent IP address to help identifying the source of the flow - AgentIp *IP `protobuf:"bytes,12,opt,name=agent_ip,json=agentIp,proto3" json:"agent_ip,omitempty"` - Flags uint32 `protobuf:"varint,13,opt,name=flags,proto3" json:"flags,omitempty"` - Icmp *Icmp `protobuf:"bytes,14,opt,name=icmp,proto3" json:"icmp,omitempty"` + AgentIp *IP `protobuf:"bytes,12,opt,name=agent_ip,json=agentIp,proto3" json:"agent_ip,omitempty"` + Flags uint32 `protobuf:"varint,13,opt,name=flags,proto3" json:"flags,omitempty"` + IcmpType uint32 `protobuf:"varint,14,opt,name=icmp_type,json=icmpType,proto3" json:"icmp_type,omitempty"` + IcmpCode uint32 `protobuf:"varint,15,opt,name=icmp_code,json=icmpCode,proto3" json:"icmp_code,omitempty"` + TcpDropBytes uint64 `protobuf:"varint,16,opt,name=tcp_drop_bytes,json=tcpDropBytes,proto3" json:"tcp_drop_bytes,omitempty"` + TcpDropPackets uint64 `protobuf:"varint,17,opt,name=tcp_drop_packets,json=tcpDropPackets,proto3" json:"tcp_drop_packets,omitempty"` + TcpDropLatestFlags uint32 `protobuf:"varint,18,opt,name=tcp_drop_latest_flags,json=tcpDropLatestFlags,proto3" json:"tcp_drop_latest_flags,omitempty"` + TcpDropLatestState uint32 `protobuf:"varint,19,opt,name=tcp_drop_latest_state,json=tcpDropLatestState,proto3" json:"tcp_drop_latest_state,omitempty"` + TcpDropLatestDropCause uint32 `protobuf:"varint,20,opt,name=tcp_drop_latest_drop_cause,json=tcpDropLatestDropCause,proto3" json:"tcp_drop_latest_drop_cause,omitempty"` + DnsId uint32 `protobuf:"varint,21,opt,name=dns_id,json=dnsId,proto3" json:"dns_id,omitempty"` + DnsFlags uint32 `protobuf:"varint,22,opt,name=dns_flags,json=dnsFlags,proto3" json:"dns_flags,omitempty"` + TimeDnsReq *timestamppb.Timestamp `protobuf:"bytes,23,opt,name=time_dns_req,json=timeDnsReq,proto3" json:"time_dns_req,omitempty"` + TimeDnsRsp *timestamppb.Timestamp `protobuf:"bytes,24,opt,name=time_dns_rsp,json=timeDnsRsp,proto3" json:"time_dns_rsp,omitempty"` } func (x *Record) Reset() { @@ -305,9 +315,79 @@ func (x *Record) GetFlags() uint32 { return 0 } -func (x *Record) GetIcmp() *Icmp { +func (x *Record) GetIcmpType() uint32 { if x != nil { - return x.Icmp + return x.IcmpType + } + return 0 +} + +func (x *Record) GetIcmpCode() uint32 { + if x != nil { + return x.IcmpCode + } + return 0 +} + +func (x *Record) GetTcpDropBytes() uint64 { + if x != nil { + return x.TcpDropBytes + } + return 0 +} + +func (x *Record) GetTcpDropPackets() uint64 { + if x != nil { + return x.TcpDropPackets + } + return 0 +} + +func (x *Record) GetTcpDropLatestFlags() uint32 { + if x != nil { + return x.TcpDropLatestFlags + } + return 0 +} + +func (x *Record) GetTcpDropLatestState() uint32 { + if x != nil { + return x.TcpDropLatestState + } + return 0 +} + +func (x *Record) GetTcpDropLatestDropCause() uint32 { + if x != nil { + return x.TcpDropLatestDropCause + } + return 0 +} + +func (x *Record) GetDnsId() uint32 { + if x != nil { + return x.DnsId + } + return 0 +} + +func (x *Record) GetDnsFlags() uint32 { + if x != nil { + return x.DnsFlags + } + return 0 +} + +func (x *Record) GetTimeDnsReq() *timestamppb.Timestamp { + if x != nil { + return x.TimeDnsReq + } + return nil +} + +func (x *Record) GetTimeDnsRsp() *timestamppb.Timestamp { + if x != nil { + return x.TimeDnsRsp } return nil } @@ -567,61 +647,6 @@ func (x *Transport) GetProtocol() uint32 { return 0 } -type Icmp struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - IcmpType uint32 `protobuf:"varint,1,opt,name=icmp_type,json=icmpType,proto3" json:"icmp_type,omitempty"` - IcmpCode uint32 `protobuf:"varint,2,opt,name=icmp_code,json=icmpCode,proto3" json:"icmp_code,omitempty"` -} - -func (x *Icmp) Reset() { - *x = Icmp{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_flow_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Icmp) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Icmp) ProtoMessage() {} - -func (x *Icmp) ProtoReflect() protoreflect.Message { - mi := &file_proto_flow_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Icmp.ProtoReflect.Descriptor instead. -func (*Icmp) Descriptor() ([]byte, []int) { - return file_proto_flow_proto_rawDescGZIP(), []int{7} -} - -func (x *Icmp) GetIcmpType() uint32 { - if x != nil { - return x.IcmpType - } - return 0 -} - -func (x *Icmp) GetIcmpCode() uint32 { - if x != nil { - return x.IcmpCode - } - return 0 -} - var File_proto_flow_proto protoreflect.FileDescriptor var file_proto_flow_proto_rawDesc = []byte{ @@ -633,7 +658,7 @@ var file_proto_flow_proto_rawDesc = []byte{ 0x07, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x28, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, - 0x65, 0x73, 0x22, 0xb6, 0x04, 0x0a, 0x06, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, + 0x65, 0x73, 0x22, 0xf0, 0x07, 0x0a, 0x06, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x65, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x2f, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, @@ -666,40 +691,64 @@ var file_proto_flow_proto_rawDesc = []byte{ 0x61, 0x67, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x70, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x70, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x20, 0x0a, 0x04, 0x69, 0x63, 0x6d, - 0x70, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, - 0x2e, 0x49, 0x63, 0x6d, 0x70, 0x52, 0x04, 0x69, 0x63, 0x6d, 0x70, 0x22, 0x3c, 0x0a, 0x08, 0x44, - 0x61, 0x74, 0x61, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x72, 0x63, 0x5f, 0x6d, - 0x61, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x72, 0x63, 0x4d, 0x61, 0x63, - 0x12, 0x17, 0x0a, 0x07, 0x64, 0x73, 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x06, 0x64, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x22, 0x57, 0x0a, 0x07, 0x4e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x25, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, - 0x49, 0x50, 0x52, 0x07, 0x73, 0x72, 0x63, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x08, 0x64, - 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, - 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x64, 0x73, 0x74, 0x41, 0x64, - 0x64, 0x72, 0x22, 0x3d, 0x0a, 0x02, 0x49, 0x50, 0x12, 0x14, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x34, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x07, 0x48, 0x00, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12, 0x14, - 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x04, - 0x69, 0x70, 0x76, 0x36, 0x42, 0x0b, 0x0a, 0x09, 0x69, 0x70, 0x5f, 0x66, 0x61, 0x6d, 0x69, 0x6c, - 0x79, 0x22, 0x5d, 0x0a, 0x09, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x19, - 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x07, 0x73, 0x72, 0x63, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x73, 0x74, - 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, 0x73, 0x74, - 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, - 0x22, 0x40, 0x0a, 0x04, 0x49, 0x63, 0x6d, 0x70, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, - 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x69, 0x63, 0x6d, - 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, 0x6f, - 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, 0x6f, - 0x64, 0x65, 0x2a, 0x24, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, - 0x45, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x01, 0x32, 0x3e, 0x0a, 0x09, 0x43, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x31, 0x0a, 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x0f, 0x2e, - 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x1a, 0x16, - 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x70, 0x62, - 0x66, 0x6c, 0x6f, 0x77, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x0d, 0x52, 0x05, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x63, 0x6d, + 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x69, 0x63, + 0x6d, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x63, 0x6d, 0x70, 0x5f, 0x63, + 0x6f, 0x64, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x69, 0x63, 0x6d, 0x70, 0x43, + 0x6f, 0x64, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x63, 0x70, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x5f, + 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x74, 0x63, 0x70, + 0x44, 0x72, 0x6f, 0x70, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x63, 0x70, + 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x11, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0e, 0x74, 0x63, 0x70, 0x44, 0x72, 0x6f, 0x70, 0x50, 0x61, 0x63, 0x6b, + 0x65, 0x74, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x74, 0x63, 0x70, 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x5f, + 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x12, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x12, 0x74, 0x63, 0x70, 0x44, 0x72, 0x6f, 0x70, 0x4c, 0x61, 0x74, 0x65, 0x73, + 0x74, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x74, 0x63, 0x70, 0x5f, 0x64, 0x72, + 0x6f, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x13, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x74, 0x63, 0x70, 0x44, 0x72, 0x6f, 0x70, 0x4c, 0x61, + 0x74, 0x65, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3a, 0x0a, 0x1a, 0x74, 0x63, 0x70, + 0x5f, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x64, 0x72, 0x6f, + 0x70, 0x5f, 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x16, 0x74, + 0x63, 0x70, 0x44, 0x72, 0x6f, 0x70, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x44, 0x72, 0x6f, 0x70, + 0x43, 0x61, 0x75, 0x73, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x64, 0x6e, 0x73, 0x5f, 0x69, 0x64, 0x18, + 0x15, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x64, 0x6e, 0x73, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, + 0x64, 0x6e, 0x73, 0x5f, 0x66, 0x6c, 0x61, 0x67, 0x73, 0x18, 0x16, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x08, 0x64, 0x6e, 0x73, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x74, 0x69, 0x6d, + 0x65, 0x5f, 0x64, 0x6e, 0x73, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x17, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x74, 0x69, 0x6d, + 0x65, 0x44, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x12, 0x3c, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x5f, + 0x64, 0x6e, 0x73, 0x5f, 0x72, 0x73, 0x70, 0x18, 0x18, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x44, + 0x6e, 0x73, 0x52, 0x73, 0x70, 0x22, 0x3c, 0x0a, 0x08, 0x44, 0x61, 0x74, 0x61, 0x4c, 0x69, 0x6e, + 0x6b, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x72, 0x63, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x06, 0x73, 0x72, 0x63, 0x4d, 0x61, 0x63, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x73, + 0x74, 0x5f, 0x6d, 0x61, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x64, 0x73, 0x74, + 0x4d, 0x61, 0x63, 0x22, 0x57, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x25, + 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x49, 0x50, 0x52, 0x07, 0x73, 0x72, + 0x63, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, + 0x2e, 0x49, 0x50, 0x52, 0x07, 0x64, 0x73, 0x74, 0x41, 0x64, 0x64, 0x72, 0x22, 0x3d, 0x0a, 0x02, + 0x49, 0x50, 0x12, 0x14, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x07, + 0x48, 0x00, 0x52, 0x04, 0x69, 0x70, 0x76, 0x34, 0x12, 0x14, 0x0a, 0x04, 0x69, 0x70, 0x76, 0x36, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x04, 0x69, 0x70, 0x76, 0x36, 0x42, 0x0b, + 0x0a, 0x09, 0x69, 0x70, 0x5f, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x22, 0x5d, 0x0a, 0x09, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x73, 0x72, 0x63, 0x50, + 0x6f, 0x72, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x64, 0x73, 0x74, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x64, 0x73, 0x74, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, + 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2a, 0x24, 0x0a, 0x09, 0x44, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x47, 0x52, 0x45, + 0x53, 0x53, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x01, + 0x32, 0x3e, 0x0a, 0x09, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x31, 0x0a, + 0x04, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x0f, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x52, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x1a, 0x16, 0x2e, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x2e, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, + 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x70, 0x62, 0x66, 0x6c, 0x6f, 0x77, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -715,7 +764,7 @@ func file_proto_flow_proto_rawDescGZIP() []byte { } var file_proto_flow_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_flow_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_proto_flow_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_proto_flow_proto_goTypes = []interface{}{ (Direction)(0), // 0: pbflow.Direction (*CollectorReply)(nil), // 1: pbflow.CollectorReply @@ -725,28 +774,28 @@ var file_proto_flow_proto_goTypes = []interface{}{ (*Network)(nil), // 5: pbflow.Network (*IP)(nil), // 6: pbflow.IP (*Transport)(nil), // 7: pbflow.Transport - (*Icmp)(nil), // 8: pbflow.Icmp - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp } var file_proto_flow_proto_depIdxs = []int32{ 3, // 0: pbflow.Records.entries:type_name -> pbflow.Record 0, // 1: pbflow.Record.direction:type_name -> pbflow.Direction - 9, // 2: pbflow.Record.time_flow_start:type_name -> google.protobuf.Timestamp - 9, // 3: pbflow.Record.time_flow_end:type_name -> google.protobuf.Timestamp + 8, // 2: pbflow.Record.time_flow_start:type_name -> google.protobuf.Timestamp + 8, // 3: pbflow.Record.time_flow_end:type_name -> google.protobuf.Timestamp 4, // 4: pbflow.Record.data_link:type_name -> pbflow.DataLink 5, // 5: pbflow.Record.network:type_name -> pbflow.Network 7, // 6: pbflow.Record.transport:type_name -> pbflow.Transport 6, // 7: pbflow.Record.agent_ip:type_name -> pbflow.IP - 8, // 8: pbflow.Record.icmp:type_name -> pbflow.Icmp - 6, // 9: pbflow.Network.src_addr:type_name -> pbflow.IP - 6, // 10: pbflow.Network.dst_addr:type_name -> pbflow.IP - 2, // 11: pbflow.Collector.Send:input_type -> pbflow.Records - 1, // 12: pbflow.Collector.Send:output_type -> pbflow.CollectorReply - 12, // [12:13] is the sub-list for method output_type - 11, // [11:12] is the sub-list for method input_type - 11, // [11:11] is the sub-list for extension type_name - 11, // [11:11] is the sub-list for extension extendee - 0, // [0:11] is the sub-list for field type_name + 8, // 8: pbflow.Record.time_dns_req:type_name -> google.protobuf.Timestamp + 8, // 9: pbflow.Record.time_dns_rsp:type_name -> google.protobuf.Timestamp + 6, // 10: pbflow.Network.src_addr:type_name -> pbflow.IP + 6, // 11: pbflow.Network.dst_addr:type_name -> pbflow.IP + 2, // 12: pbflow.Collector.Send:input_type -> pbflow.Records + 1, // 13: pbflow.Collector.Send:output_type -> pbflow.CollectorReply + 13, // [13:14] is the sub-list for method output_type + 12, // [12:13] is the sub-list for method input_type + 12, // [12:12] is the sub-list for extension type_name + 12, // [12:12] is the sub-list for extension extendee + 0, // [0:12] is the sub-list for field type_name } func init() { file_proto_flow_proto_init() } @@ -839,18 +888,6 @@ func file_proto_flow_proto_init() { return nil } } - file_proto_flow_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Icmp); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } } file_proto_flow_proto_msgTypes[5].OneofWrappers = []interface{}{ (*IP_Ipv4)(nil), @@ -862,7 +899,7 @@ func file_proto_flow_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_flow_proto_rawDesc, NumEnums: 1, - NumMessages: 8, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/flow.proto b/proto/flow.proto index c7b459a1d..fb854059f 100644 --- a/proto/flow.proto +++ b/proto/flow.proto @@ -41,7 +41,17 @@ message Record { // Agent IP address to help identifying the source of the flow IP agent_ip = 12; uint32 flags = 13; - Icmp icmp = 14; + uint32 icmp_type = 14; + uint32 icmp_code = 15; + uint64 tcp_drop_bytes = 16; + uint64 tcp_drop_packets = 17; + uint32 tcp_drop_latest_flags = 18; + uint32 tcp_drop_latest_state = 19; + uint32 tcp_drop_latest_drop_cause = 20; + uint32 dns_id = 21; + uint32 dns_flags = 22; + google.protobuf.Timestamp time_dns_req = 23; + google.protobuf.Timestamp time_dns_rsp = 24; } message DataLink { @@ -69,11 +79,6 @@ message Transport { uint32 protocol = 3; } -message Icmp { - uint32 icmp_type = 1; - uint32 icmp_code = 2; -} - // as defined by field 61 in // https://www.iana.org/assignments/ipfix/ipfix.xhtml enum Direction { diff --git a/vendor/github.com/cilium/ebpf/link/cgroup.go b/vendor/github.com/cilium/ebpf/link/cgroup.go new file mode 100644 index 000000000..bfad1cced --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/cgroup.go @@ -0,0 +1,165 @@ +package link + +import ( + "errors" + "fmt" + "os" + + "github.com/cilium/ebpf" +) + +type cgroupAttachFlags uint32 + +// cgroup attach flags +const ( + flagAllowOverride cgroupAttachFlags = 1 << iota + flagAllowMulti + flagReplace +) + +type CgroupOptions struct { + // Path to a cgroupv2 folder. + Path string + // One of the AttachCgroup* constants + Attach ebpf.AttachType + // Program must be of type CGroup*, and the attach type must match Attach. + Program *ebpf.Program +} + +// AttachCgroup links a BPF program to a cgroup. +func AttachCgroup(opts CgroupOptions) (Link, error) { + cgroup, err := os.Open(opts.Path) + if err != nil { + return nil, fmt.Errorf("can't open cgroup: %s", err) + } + + clone, err := opts.Program.Clone() + if err != nil { + cgroup.Close() + return nil, err + } + + var cg Link + cg, err = newLinkCgroup(cgroup, opts.Attach, clone) + if errors.Is(err, ErrNotSupported) { + cg, err = newProgAttachCgroup(cgroup, opts.Attach, clone, flagAllowMulti) + } + if errors.Is(err, ErrNotSupported) { + cg, err = newProgAttachCgroup(cgroup, opts.Attach, clone, flagAllowOverride) + } + if err != nil { + cgroup.Close() + clone.Close() + return nil, err + } + + return cg, nil +} + +type progAttachCgroup struct { + cgroup *os.File + current *ebpf.Program + attachType ebpf.AttachType + flags cgroupAttachFlags +} + +var _ Link = (*progAttachCgroup)(nil) + +func (cg *progAttachCgroup) isLink() {} + +func newProgAttachCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program, flags cgroupAttachFlags) (*progAttachCgroup, error) { + if flags&flagAllowMulti > 0 { + if err := haveProgAttachReplace(); err != nil { + return nil, fmt.Errorf("can't support multiple programs: %w", err) + } + } + + err := RawAttachProgram(RawAttachProgramOptions{ + Target: int(cgroup.Fd()), + Program: prog, + Flags: uint32(flags), + Attach: attach, + }) + if err != nil { + return nil, fmt.Errorf("cgroup: %w", err) + } + + return &progAttachCgroup{cgroup, prog, attach, flags}, nil +} + +func (cg *progAttachCgroup) Close() error { + defer cg.cgroup.Close() + defer cg.current.Close() + + err := RawDetachProgram(RawDetachProgramOptions{ + Target: int(cg.cgroup.Fd()), + Program: cg.current, + Attach: cg.attachType, + }) + if err != nil { + return fmt.Errorf("close cgroup: %s", err) + } + return nil +} + +func (cg *progAttachCgroup) Update(prog *ebpf.Program) error { + new, err := prog.Clone() + if err != nil { + return err + } + + args := RawAttachProgramOptions{ + Target: int(cg.cgroup.Fd()), + Program: prog, + Attach: cg.attachType, + Flags: uint32(cg.flags), + } + + if cg.flags&flagAllowMulti > 0 { + // Atomically replacing multiple programs requires at least + // 5.5 (commit 7dd68b3279f17921 "bpf: Support replacing cgroup-bpf + // program in MULTI mode") + args.Flags |= uint32(flagReplace) + args.Replace = cg.current + } + + if err := RawAttachProgram(args); err != nil { + new.Close() + return fmt.Errorf("can't update cgroup: %s", err) + } + + cg.current.Close() + cg.current = new + return nil +} + +func (cg *progAttachCgroup) Pin(string) error { + return fmt.Errorf("can't pin cgroup: %w", ErrNotSupported) +} + +func (cg *progAttachCgroup) Unpin() error { + return fmt.Errorf("can't unpin cgroup: %w", ErrNotSupported) +} + +func (cg *progAttachCgroup) Info() (*Info, error) { + return nil, fmt.Errorf("can't get cgroup info: %w", ErrNotSupported) +} + +type linkCgroup struct { + RawLink +} + +var _ Link = (*linkCgroup)(nil) + +func newLinkCgroup(cgroup *os.File, attach ebpf.AttachType, prog *ebpf.Program) (*linkCgroup, error) { + link, err := AttachRawLink(RawLinkOptions{ + Target: int(cgroup.Fd()), + Program: prog, + Attach: attach, + }) + if err != nil { + return nil, err + } + + return &linkCgroup{*link}, err +} diff --git a/vendor/github.com/cilium/ebpf/link/doc.go b/vendor/github.com/cilium/ebpf/link/doc.go new file mode 100644 index 000000000..2bde35ed7 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/doc.go @@ -0,0 +1,2 @@ +// Package link allows attaching eBPF programs to various kernel hooks. +package link diff --git a/vendor/github.com/cilium/ebpf/link/iter.go b/vendor/github.com/cilium/ebpf/link/iter.go new file mode 100644 index 000000000..d2b32ef33 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/iter.go @@ -0,0 +1,85 @@ +package link + +import ( + "fmt" + "io" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/sys" +) + +type IterOptions struct { + // Program must be of type Tracing with attach type + // AttachTraceIter. The kind of iterator to attach to is + // determined at load time via the AttachTo field. + // + // AttachTo requires the kernel to include BTF of itself, + // and it to be compiled with a recent pahole (>= 1.16). + Program *ebpf.Program + + // Map specifies the target map for bpf_map_elem and sockmap iterators. + // It may be nil. + Map *ebpf.Map +} + +// AttachIter attaches a BPF seq_file iterator. +func AttachIter(opts IterOptions) (*Iter, error) { + if err := haveBPFLink(); err != nil { + return nil, err + } + + progFd := opts.Program.FD() + if progFd < 0 { + return nil, fmt.Errorf("invalid program: %s", sys.ErrClosedFd) + } + + var info bpfIterLinkInfoMap + if opts.Map != nil { + mapFd := opts.Map.FD() + if mapFd < 0 { + return nil, fmt.Errorf("invalid map: %w", sys.ErrClosedFd) + } + info.map_fd = uint32(mapFd) + } + + attr := sys.LinkCreateIterAttr{ + ProgFd: uint32(progFd), + AttachType: sys.AttachType(ebpf.AttachTraceIter), + IterInfo: sys.NewPointer(unsafe.Pointer(&info)), + IterInfoLen: uint32(unsafe.Sizeof(info)), + } + + fd, err := sys.LinkCreateIter(&attr) + if err != nil { + return nil, fmt.Errorf("can't link iterator: %w", err) + } + + return &Iter{RawLink{fd, ""}}, err +} + +// Iter represents an attached bpf_iter. +type Iter struct { + RawLink +} + +// Open creates a new instance of the iterator. +// +// Reading from the returned reader triggers the BPF program. +func (it *Iter) Open() (io.ReadCloser, error) { + attr := &sys.IterCreateAttr{ + LinkFd: it.fd.Uint(), + } + + fd, err := sys.IterCreate(attr) + if err != nil { + return nil, fmt.Errorf("can't create iterator: %w", err) + } + + return fd.File("bpf_iter"), nil +} + +// union bpf_iter_link_info.map +type bpfIterLinkInfoMap struct { + map_fd uint32 +} diff --git a/vendor/github.com/cilium/ebpf/link/kprobe.go b/vendor/github.com/cilium/ebpf/link/kprobe.go new file mode 100644 index 000000000..9ce7eb4a4 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/kprobe.go @@ -0,0 +1,574 @@ +package link + +import ( + "crypto/rand" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "syscall" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/sys" + "github.com/cilium/ebpf/internal/unix" +) + +var ( + kprobeEventsPath = filepath.Join(tracefsPath, "kprobe_events") +) + +type probeType uint8 + +type probeArgs struct { + symbol, group, path string + offset, refCtrOffset, cookie uint64 + pid, retprobeMaxActive int + ret bool +} + +// KprobeOptions defines additional parameters that will be used +// when loading Kprobes. +type KprobeOptions struct { + // Arbitrary value that can be fetched from an eBPF program + // via `bpf_get_attach_cookie()`. + // + // Needs kernel 5.15+. + Cookie uint64 + // Offset of the kprobe relative to the traced symbol. + // Can be used to insert kprobes at arbitrary offsets in kernel functions, + // e.g. in places where functions have been inlined. + Offset uint64 + // Increase the maximum number of concurrent invocations of a kretprobe. + // Required when tracing some long running functions in the kernel. + // + // Deprecated: this setting forces the use of an outdated kernel API and is not portable + // across kernel versions. + RetprobeMaxActive int +} + +const ( + kprobeType probeType = iota + uprobeType +) + +func (pt probeType) String() string { + if pt == kprobeType { + return "kprobe" + } + return "uprobe" +} + +func (pt probeType) EventsPath() string { + if pt == kprobeType { + return kprobeEventsPath + } + return uprobeEventsPath +} + +func (pt probeType) PerfEventType(ret bool) perfEventType { + if pt == kprobeType { + if ret { + return kretprobeEvent + } + return kprobeEvent + } + if ret { + return uretprobeEvent + } + return uprobeEvent +} + +// Kprobe attaches the given eBPF program to a perf event that fires when the +// given kernel symbol starts executing. See /proc/kallsyms for available +// symbols. For example, printk(): +// +// kp, err := Kprobe("printk", prog, nil) +// +// Losing the reference to the resulting Link (kp) will close the Kprobe +// and prevent further execution of prog. The Link must be Closed during +// program shutdown to avoid leaking system resources. +func Kprobe(symbol string, prog *ebpf.Program, opts *KprobeOptions) (Link, error) { + k, err := kprobe(symbol, prog, opts, false) + if err != nil { + return nil, err + } + + lnk, err := attachPerfEvent(k, prog) + if err != nil { + k.Close() + return nil, err + } + + return lnk, nil +} + +// Kretprobe attaches the given eBPF program to a perf event that fires right +// before the given kernel symbol exits, with the function stack left intact. +// See /proc/kallsyms for available symbols. For example, printk(): +// +// kp, err := Kretprobe("printk", prog, nil) +// +// Losing the reference to the resulting Link (kp) will close the Kretprobe +// and prevent further execution of prog. The Link must be Closed during +// program shutdown to avoid leaking system resources. +// +// On kernels 5.10 and earlier, setting a kretprobe on a nonexistent symbol +// incorrectly returns unix.EINVAL instead of os.ErrNotExist. +func Kretprobe(symbol string, prog *ebpf.Program, opts *KprobeOptions) (Link, error) { + k, err := kprobe(symbol, prog, opts, true) + if err != nil { + return nil, err + } + + lnk, err := attachPerfEvent(k, prog) + if err != nil { + k.Close() + return nil, err + } + + return lnk, nil +} + +// isValidKprobeSymbol implements the equivalent of a regex match +// against "^[a-zA-Z_][0-9a-zA-Z_.]*$". +func isValidKprobeSymbol(s string) bool { + if len(s) < 1 { + return false + } + + for i, c := range []byte(s) { + switch { + case c >= 'a' && c <= 'z': + case c >= 'A' && c <= 'Z': + case c == '_': + case i > 0 && c >= '0' && c <= '9': + + // Allow `.` in symbol name. GCC-compiled kernel may change symbol name + // to have a `.isra.$n` suffix, like `udp_send_skb.isra.52`. + // See: https://gcc.gnu.org/gcc-10/changes.html + case i > 0 && c == '.': + + default: + return false + } + } + + return true +} + +// kprobe opens a perf event on the given symbol and attaches prog to it. +// If ret is true, create a kretprobe. +func kprobe(symbol string, prog *ebpf.Program, opts *KprobeOptions, ret bool) (*perfEvent, error) { + if symbol == "" { + return nil, fmt.Errorf("symbol name cannot be empty: %w", errInvalidInput) + } + if prog == nil { + return nil, fmt.Errorf("prog cannot be nil: %w", errInvalidInput) + } + if !isValidKprobeSymbol(symbol) { + return nil, fmt.Errorf("symbol '%s' must be a valid symbol in /proc/kallsyms: %w", symbol, errInvalidInput) + } + if prog.Type() != ebpf.Kprobe { + return nil, fmt.Errorf("eBPF program type %s is not a Kprobe: %w", prog.Type(), errInvalidInput) + } + + args := probeArgs{ + pid: perfAllThreads, + symbol: symbol, + ret: ret, + } + + if opts != nil { + args.retprobeMaxActive = opts.RetprobeMaxActive + args.cookie = opts.Cookie + args.offset = opts.Offset + } + + // Use kprobe PMU if the kernel has it available. + tp, err := pmuKprobe(args) + if errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.EINVAL) { + args.symbol = platformPrefix(symbol) + tp, err = pmuKprobe(args) + } + if err == nil { + return tp, nil + } + if err != nil && !errors.Is(err, ErrNotSupported) { + return nil, fmt.Errorf("creating perf_kprobe PMU: %w", err) + } + + // Use tracefs if kprobe PMU is missing. + args.symbol = symbol + tp, err = tracefsKprobe(args) + if errors.Is(err, os.ErrNotExist) || errors.Is(err, unix.EINVAL) { + args.symbol = platformPrefix(symbol) + tp, err = tracefsKprobe(args) + } + if err != nil { + return nil, fmt.Errorf("creating trace event '%s' in tracefs: %w", symbol, err) + } + + return tp, nil +} + +// pmuKprobe opens a perf event based on the kprobe PMU. +// Returns os.ErrNotExist if the given symbol does not exist in the kernel. +func pmuKprobe(args probeArgs) (*perfEvent, error) { + return pmuProbe(kprobeType, args) +} + +// pmuProbe opens a perf event based on a Performance Monitoring Unit. +// +// Requires at least a 4.17 kernel. +// e12f03d7031a "perf/core: Implement the 'perf_kprobe' PMU" +// 33ea4b24277b "perf/core: Implement the 'perf_uprobe' PMU" +// +// Returns ErrNotSupported if the kernel doesn't support perf_[k,u]probe PMU +func pmuProbe(typ probeType, args probeArgs) (*perfEvent, error) { + // Getting the PMU type will fail if the kernel doesn't support + // the perf_[k,u]probe PMU. + et, err := readUint64FromFileOnce("%d\n", "/sys/bus/event_source/devices", typ.String(), "type") + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("%s: %w", typ, ErrNotSupported) + } + if err != nil { + return nil, err + } + + // Use tracefs if we want to set kretprobe's retprobeMaxActive. + if args.retprobeMaxActive != 0 { + return nil, fmt.Errorf("pmu probe: non-zero retprobeMaxActive: %w", ErrNotSupported) + } + + var config uint64 + if args.ret { + bit, err := readUint64FromFileOnce("config:%d\n", "/sys/bus/event_source/devices", typ.String(), "/format/retprobe") + if err != nil { + return nil, err + } + config |= 1 << bit + } + + var ( + attr unix.PerfEventAttr + sp unsafe.Pointer + token string + ) + switch typ { + case kprobeType: + // Create a pointer to a NUL-terminated string for the kernel. + sp, err = unsafeStringPtr(args.symbol) + if err != nil { + return nil, err + } + + token = kprobeToken(args) + + attr = unix.PerfEventAttr{ + // The minimum size required for PMU kprobes is PERF_ATTR_SIZE_VER1, + // since it added the config2 (Ext2) field. Use Ext2 as probe_offset. + Size: unix.PERF_ATTR_SIZE_VER1, + Type: uint32(et), // PMU event type read from sysfs + Ext1: uint64(uintptr(sp)), // Kernel symbol to trace + Ext2: args.offset, // Kernel symbol offset + Config: config, // Retprobe flag + } + case uprobeType: + sp, err = unsafeStringPtr(args.path) + if err != nil { + return nil, err + } + + if args.refCtrOffset != 0 { + config |= args.refCtrOffset << uprobeRefCtrOffsetShift + } + + token = uprobeToken(args) + + attr = unix.PerfEventAttr{ + // The minimum size required for PMU uprobes is PERF_ATTR_SIZE_VER1, + // since it added the config2 (Ext2) field. The Size field controls the + // size of the internal buffer the kernel allocates for reading the + // perf_event_attr argument from userspace. + Size: unix.PERF_ATTR_SIZE_VER1, + Type: uint32(et), // PMU event type read from sysfs + Ext1: uint64(uintptr(sp)), // Uprobe path + Ext2: args.offset, // Uprobe offset + Config: config, // RefCtrOffset, Retprobe flag + } + } + + rawFd, err := unix.PerfEventOpen(&attr, args.pid, 0, -1, unix.PERF_FLAG_FD_CLOEXEC) + + // On some old kernels, kprobe PMU doesn't allow `.` in symbol names and + // return -EINVAL. Return ErrNotSupported to allow falling back to tracefs. + // https://github.com/torvalds/linux/blob/94710cac0ef4/kernel/trace/trace_kprobe.c#L340-L343 + if errors.Is(err, unix.EINVAL) && strings.Contains(args.symbol, ".") { + return nil, fmt.Errorf("token %s: older kernels don't accept dots: %w", token, ErrNotSupported) + } + // Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL + // when trying to create a retprobe for a missing symbol. + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("token %s: not found: %w", token, err) + } + // Since commit ab105a4fb894, EILSEQ is returned when a kprobe sym+offset is resolved + // to an invalid insn boundary. The exact conditions that trigger this error are + // arch specific however. + if errors.Is(err, unix.EILSEQ) { + return nil, fmt.Errorf("token %s: bad insn boundary: %w", token, os.ErrNotExist) + } + // Since at least commit cb9a19fe4aa51, ENOTSUPP is returned + // when attempting to set a uprobe on a trap instruction. + if errors.Is(err, sys.ENOTSUPP) { + return nil, fmt.Errorf("token %s: failed setting uprobe on offset %#x (possible trap insn): %w", token, args.offset, err) + } + + if err != nil { + return nil, fmt.Errorf("token %s: opening perf event: %w", token, err) + } + + // Ensure the string pointer is not collected before PerfEventOpen returns. + runtime.KeepAlive(sp) + + fd, err := sys.NewFD(rawFd) + if err != nil { + return nil, err + } + + // Kernel has perf_[k,u]probe PMU available, initialize perf event. + return &perfEvent{ + typ: typ.PerfEventType(args.ret), + name: args.symbol, + pmuID: et, + cookie: args.cookie, + fd: fd, + }, nil +} + +// tracefsKprobe creates a Kprobe tracefs entry. +func tracefsKprobe(args probeArgs) (*perfEvent, error) { + return tracefsProbe(kprobeType, args) +} + +// tracefsProbe creates a trace event by writing an entry to <tracefs>/[k,u]probe_events. +// A new trace event group name is generated on every call to support creating +// multiple trace events for the same kernel or userspace symbol. +// Path and offset are only set in the case of uprobe(s) and are used to set +// the executable/library path on the filesystem and the offset where the probe is inserted. +// A perf event is then opened on the newly-created trace event and returned to the caller. +func tracefsProbe(typ probeType, args probeArgs) (*perfEvent, error) { + // Generate a random string for each trace event we attempt to create. + // This value is used as the 'group' token in tracefs to allow creating + // multiple kprobe trace events with the same name. + group, err := randomGroup("ebpf") + if err != nil { + return nil, fmt.Errorf("randomizing group name: %w", err) + } + args.group = group + + // Create the [k,u]probe trace event using tracefs. + tid, err := createTraceFSProbeEvent(typ, args) + if err != nil { + return nil, fmt.Errorf("creating probe entry on tracefs: %w", err) + } + + // Kprobes are ephemeral tracepoints and share the same perf event type. + fd, err := openTracepointPerfEvent(tid, args.pid) + if err != nil { + // Make sure we clean up the created tracefs event when we return error. + // If a livepatch handler is already active on the symbol, the write to + // tracefs will succeed, a trace event will show up, but creating the + // perf event will fail with EBUSY. + _ = closeTraceFSProbeEvent(typ, args.group, args.symbol) + return nil, err + } + + return &perfEvent{ + typ: typ.PerfEventType(args.ret), + group: group, + name: args.symbol, + tracefsID: tid, + cookie: args.cookie, + fd: fd, + }, nil +} + +var errInvalidMaxActive = errors.New("can only set maxactive on kretprobes") + +// createTraceFSProbeEvent creates a new ephemeral trace event. +// +// Returns os.ErrNotExist if symbol is not a valid +// kernel symbol, or if it is not traceable with kprobes. Returns os.ErrExist +// if a probe with the same group and symbol already exists. Returns an error if +// args.retprobeMaxActive is used on non kprobe types. Returns ErrNotSupported if +// the kernel is too old to support kretprobe maxactive. +func createTraceFSProbeEvent(typ probeType, args probeArgs) (uint64, error) { + // Before attempting to create a trace event through tracefs, + // check if an event with the same group and name already exists. + // Kernels 4.x and earlier don't return os.ErrExist on writing a duplicate + // entry, so we need to rely on reads for detecting uniqueness. + _, err := getTraceEventID(args.group, args.symbol) + if err == nil { + return 0, fmt.Errorf("trace event %s/%s: %w", args.group, args.symbol, os.ErrExist) + } + if err != nil && !errors.Is(err, os.ErrNotExist) { + return 0, fmt.Errorf("checking trace event %s/%s: %w", args.group, args.symbol, err) + } + + // Open the kprobe_events file in tracefs. + f, err := os.OpenFile(typ.EventsPath(), os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { + return 0, fmt.Errorf("error opening '%s': %w", typ.EventsPath(), err) + } + defer f.Close() + + var pe, token string + switch typ { + case kprobeType: + // The kprobe_events syntax is as follows (see Documentation/trace/kprobetrace.txt): + // p[:[GRP/]EVENT] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS] : Set a probe + // r[MAXACTIVE][:[GRP/]EVENT] [MOD:]SYM[+0] [FETCHARGS] : Set a return probe + // -:[GRP/]EVENT : Clear a probe + // + // Some examples: + // r:ebpf_1234/r_my_kretprobe nf_conntrack_destroy + // p:ebpf_5678/p_my_kprobe __x64_sys_execve + // + // Leaving the kretprobe's MAXACTIVE set to 0 (or absent) will make the + // kernel default to NR_CPUS. This is desired in most eBPF cases since + // subsampling or rate limiting logic can be more accurately implemented in + // the eBPF program itself. + // See Documentation/kprobes.txt for more details. + if args.retprobeMaxActive != 0 && !args.ret { + return 0, errInvalidMaxActive + } + token = kprobeToken(args) + pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(args.ret, args.retprobeMaxActive), args.group, sanitizeSymbol(args.symbol), token) + case uprobeType: + // The uprobe_events syntax is as follows: + // p[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a probe + // r[:[GRP/]EVENT] PATH:OFFSET [FETCHARGS] : Set a return probe + // -:[GRP/]EVENT : Clear a probe + // + // Some examples: + // r:ebpf_1234/readline /bin/bash:0x12345 + // p:ebpf_5678/main_mySymbol /bin/mybin:0x12345(0x123) + // + // See Documentation/trace/uprobetracer.txt for more details. + if args.retprobeMaxActive != 0 { + return 0, errInvalidMaxActive + } + token = uprobeToken(args) + pe = fmt.Sprintf("%s:%s/%s %s", probePrefix(args.ret, 0), args.group, args.symbol, token) + } + _, err = f.WriteString(pe) + + // Since commit 97c753e62e6c, ENOENT is correctly returned instead of EINVAL + // when trying to create a retprobe for a missing symbol. + if errors.Is(err, os.ErrNotExist) { + return 0, fmt.Errorf("token %s: not found: %w", token, err) + } + // Since commit ab105a4fb894, EILSEQ is returned when a kprobe sym+offset is resolved + // to an invalid insn boundary. The exact conditions that trigger this error are + // arch specific however. + if errors.Is(err, syscall.EILSEQ) { + return 0, fmt.Errorf("token %s: bad insn boundary: %w", token, os.ErrNotExist) + } + // ERANGE is returned when the `SYM[+offs]` token is too big and cannot + // be resolved. + if errors.Is(err, syscall.ERANGE) { + return 0, fmt.Errorf("token %s: offset too big: %w", token, os.ErrNotExist) + } + + if err != nil { + return 0, fmt.Errorf("token %s: writing '%s': %w", token, pe, err) + } + + // Get the newly-created trace event's id. + tid, err := getTraceEventID(args.group, args.symbol) + if args.retprobeMaxActive != 0 && errors.Is(err, os.ErrNotExist) { + // Kernels < 4.12 don't support maxactive and therefore auto generate + // group and event names from the symbol and offset. The symbol is used + // without any sanitization. + // See https://elixir.bootlin.com/linux/v4.10/source/kernel/trace/trace_kprobe.c#L712 + event := fmt.Sprintf("kprobes/r_%s_%d", args.symbol, args.offset) + if err := removeTraceFSProbeEvent(typ, event); err != nil { + return 0, fmt.Errorf("failed to remove spurious maxactive event: %s", err) + } + return 0, fmt.Errorf("create trace event with non-default maxactive: %w", ErrNotSupported) + } + if err != nil { + return 0, fmt.Errorf("get trace event id: %w", err) + } + + return tid, nil +} + +// closeTraceFSProbeEvent removes the [k,u]probe with the given type, group and symbol +// from <tracefs>/[k,u]probe_events. +func closeTraceFSProbeEvent(typ probeType, group, symbol string) error { + pe := fmt.Sprintf("%s/%s", group, sanitizeSymbol(symbol)) + return removeTraceFSProbeEvent(typ, pe) +} + +func removeTraceFSProbeEvent(typ probeType, pe string) error { + f, err := os.OpenFile(typ.EventsPath(), os.O_APPEND|os.O_WRONLY, 0666) + if err != nil { + return fmt.Errorf("error opening %s: %w", typ.EventsPath(), err) + } + defer f.Close() + + // See [k,u]probe_events syntax above. The probe type does not need to be specified + // for removals. + if _, err = f.WriteString("-:" + pe); err != nil { + return fmt.Errorf("remove event %q from %s: %w", pe, typ.EventsPath(), err) + } + + return nil +} + +// randomGroup generates a pseudorandom string for use as a tracefs group name. +// Returns an error when the output string would exceed 63 characters (kernel +// limitation), when rand.Read() fails or when prefix contains characters not +// allowed by isValidTraceID. +func randomGroup(prefix string) (string, error) { + if !isValidTraceID(prefix) { + return "", fmt.Errorf("prefix '%s' must be alphanumeric or underscore: %w", prefix, errInvalidInput) + } + + b := make([]byte, 8) + if _, err := rand.Read(b); err != nil { + return "", fmt.Errorf("reading random bytes: %w", err) + } + + group := fmt.Sprintf("%s_%x", prefix, b) + if len(group) > 63 { + return "", fmt.Errorf("group name '%s' cannot be longer than 63 characters: %w", group, errInvalidInput) + } + + return group, nil +} + +func probePrefix(ret bool, maxActive int) string { + if ret { + if maxActive > 0 { + return fmt.Sprintf("r%d", maxActive) + } + return "r" + } + return "p" +} + +// kprobeToken creates the SYM[+offs] token for the tracefs api. +func kprobeToken(args probeArgs) string { + po := args.symbol + + if args.offset != 0 { + po += fmt.Sprintf("+%#x", args.offset) + } + + return po +} diff --git a/vendor/github.com/cilium/ebpf/link/kprobe_multi.go b/vendor/github.com/cilium/ebpf/link/kprobe_multi.go new file mode 100644 index 000000000..151f47d66 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/kprobe_multi.go @@ -0,0 +1,180 @@ +package link + +import ( + "errors" + "fmt" + "os" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/sys" + "github.com/cilium/ebpf/internal/unix" +) + +// KprobeMultiOptions defines additional parameters that will be used +// when opening a KprobeMulti Link. +type KprobeMultiOptions struct { + // Symbols takes a list of kernel symbol names to attach an ebpf program to. + // + // Mutually exclusive with Addresses. + Symbols []string + + // Addresses takes a list of kernel symbol addresses in case they can not + // be referred to by name. + // + // Note that only start addresses can be specified, since the fprobe API + // limits the attach point to the function entry or return. + // + // Mutually exclusive with Symbols. + Addresses []uint64 + + // Cookies specifies arbitrary values that can be fetched from an eBPF + // program via `bpf_get_attach_cookie()`. + // + // If set, its length should be equal to the length of Symbols or Addresses. + // Each Cookie is assigned to the Symbol or Address specified at the + // corresponding slice index. + Cookies []uint64 +} + +// KprobeMulti attaches the given eBPF program to the entry point of a given set +// of kernel symbols. +// +// The difference with Kprobe() is that multi-kprobe accomplishes this in a +// single system call, making it significantly faster than attaching many +// probes one at a time. +// +// Requires at least Linux 5.18. +func KprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions) (Link, error) { + return kprobeMulti(prog, opts, 0) +} + +// KretprobeMulti attaches the given eBPF program to the return point of a given +// set of kernel symbols. +// +// The difference with Kretprobe() is that multi-kprobe accomplishes this in a +// single system call, making it significantly faster than attaching many +// probes one at a time. +// +// Requires at least Linux 5.18. +func KretprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions) (Link, error) { + return kprobeMulti(prog, opts, unix.BPF_F_KPROBE_MULTI_RETURN) +} + +func kprobeMulti(prog *ebpf.Program, opts KprobeMultiOptions, flags uint32) (Link, error) { + if prog == nil { + return nil, errors.New("cannot attach a nil program") + } + + syms := uint32(len(opts.Symbols)) + addrs := uint32(len(opts.Addresses)) + cookies := uint32(len(opts.Cookies)) + + if syms == 0 && addrs == 0 { + return nil, fmt.Errorf("one of Symbols or Addresses is required: %w", errInvalidInput) + } + if syms != 0 && addrs != 0 { + return nil, fmt.Errorf("Symbols and Addresses are mutually exclusive: %w", errInvalidInput) + } + if cookies > 0 && cookies != syms && cookies != addrs { + return nil, fmt.Errorf("Cookies must be exactly Symbols or Addresses in length: %w", errInvalidInput) + } + + if err := haveBPFLinkKprobeMulti(); err != nil { + return nil, err + } + + attr := &sys.LinkCreateKprobeMultiAttr{ + ProgFd: uint32(prog.FD()), + AttachType: sys.BPF_TRACE_KPROBE_MULTI, + KprobeMultiFlags: flags, + } + + switch { + case syms != 0: + attr.Count = syms + attr.Syms = sys.NewStringSlicePointer(opts.Symbols) + + case addrs != 0: + attr.Count = addrs + attr.Addrs = sys.NewPointer(unsafe.Pointer(&opts.Addresses[0])) + } + + if cookies != 0 { + attr.Cookies = sys.NewPointer(unsafe.Pointer(&opts.Cookies[0])) + } + + fd, err := sys.LinkCreateKprobeMulti(attr) + if errors.Is(err, unix.ESRCH) { + return nil, fmt.Errorf("couldn't find one or more symbols: %w", os.ErrNotExist) + } + if errors.Is(err, unix.EINVAL) { + return nil, fmt.Errorf("%w (missing kernel symbol or prog's AttachType not AttachTraceKprobeMulti?)", err) + } + if err != nil { + return nil, err + } + + return &kprobeMultiLink{RawLink{fd, ""}}, nil +} + +type kprobeMultiLink struct { + RawLink +} + +var _ Link = (*kprobeMultiLink)(nil) + +func (kml *kprobeMultiLink) Update(prog *ebpf.Program) error { + return fmt.Errorf("update kprobe_multi: %w", ErrNotSupported) +} + +func (kml *kprobeMultiLink) Pin(string) error { + return fmt.Errorf("pin kprobe_multi: %w", ErrNotSupported) +} + +func (kml *kprobeMultiLink) Unpin() error { + return fmt.Errorf("unpin kprobe_multi: %w", ErrNotSupported) +} + +var haveBPFLinkKprobeMulti = internal.NewFeatureTest("bpf_link_kprobe_multi", "5.18", func() error { + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Name: "probe_kpm_link", + Type: ebpf.Kprobe, + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + AttachType: ebpf.AttachTraceKprobeMulti, + License: "MIT", + }) + if errors.Is(err, unix.E2BIG) { + // Kernel doesn't support AttachType field. + return internal.ErrNotSupported + } + if err != nil { + return err + } + defer prog.Close() + + fd, err := sys.LinkCreateKprobeMulti(&sys.LinkCreateKprobeMultiAttr{ + ProgFd: uint32(prog.FD()), + AttachType: sys.BPF_TRACE_KPROBE_MULTI, + Count: 1, + Syms: sys.NewStringSlicePointer([]string{"vprintk"}), + }) + switch { + case errors.Is(err, unix.EINVAL): + return internal.ErrNotSupported + // If CONFIG_FPROBE isn't set. + case errors.Is(err, unix.EOPNOTSUPP): + return internal.ErrNotSupported + case err != nil: + return err + } + + fd.Close() + + return nil +}) diff --git a/vendor/github.com/cilium/ebpf/link/link.go b/vendor/github.com/cilium/ebpf/link/link.go new file mode 100644 index 000000000..d4eeb92de --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/link.go @@ -0,0 +1,315 @@ +package link + +import ( + "bytes" + "encoding/binary" + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/btf" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/sys" +) + +var ErrNotSupported = internal.ErrNotSupported + +// Link represents a Program attached to a BPF hook. +type Link interface { + // Replace the current program with a new program. + // + // Passing a nil program is an error. May return an error wrapping ErrNotSupported. + Update(*ebpf.Program) error + + // Persist a link by pinning it into a bpffs. + // + // May return an error wrapping ErrNotSupported. + Pin(string) error + + // Undo a previous call to Pin. + // + // May return an error wrapping ErrNotSupported. + Unpin() error + + // Close frees resources. + // + // The link will be broken unless it has been successfully pinned. + // A link may continue past the lifetime of the process if Close is + // not called. + Close() error + + // Info returns metadata on a link. + // + // May return an error wrapping ErrNotSupported. + Info() (*Info, error) + + // Prevent external users from implementing this interface. + isLink() +} + +// LoadPinnedLink loads a link that was persisted into a bpffs. +func LoadPinnedLink(fileName string, opts *ebpf.LoadPinOptions) (Link, error) { + raw, err := loadPinnedRawLink(fileName, opts) + if err != nil { + return nil, err + } + + return wrapRawLink(raw) +} + +// wrap a RawLink in a more specific type if possible. +// +// The function takes ownership of raw and closes it on error. +func wrapRawLink(raw *RawLink) (Link, error) { + info, err := raw.Info() + if err != nil { + raw.Close() + return nil, err + } + + switch info.Type { + case RawTracepointType: + return &rawTracepoint{*raw}, nil + case TracingType: + return &tracing{*raw}, nil + case CgroupType: + return &linkCgroup{*raw}, nil + case IterType: + return &Iter{*raw}, nil + case NetNsType: + return &NetNsLink{*raw}, nil + default: + return raw, nil + } +} + +// ID uniquely identifies a BPF link. +type ID = sys.LinkID + +// RawLinkOptions control the creation of a raw link. +type RawLinkOptions struct { + // File descriptor to attach to. This differs for each attach type. + Target int + // Program to attach. + Program *ebpf.Program + // Attach must match the attach type of Program. + Attach ebpf.AttachType + // BTF is the BTF of the attachment target. + BTF btf.TypeID + // Flags control the attach behaviour. + Flags uint32 +} + +// Info contains metadata on a link. +type Info struct { + Type Type + ID ID + Program ebpf.ProgramID + extra interface{} +} + +type TracingInfo sys.TracingLinkInfo +type CgroupInfo sys.CgroupLinkInfo +type NetNsInfo sys.NetNsLinkInfo +type XDPInfo sys.XDPLinkInfo + +// Tracing returns tracing type-specific link info. +// +// Returns nil if the type-specific link info isn't available. +func (r Info) Tracing() *TracingInfo { + e, _ := r.extra.(*TracingInfo) + return e +} + +// Cgroup returns cgroup type-specific link info. +// +// Returns nil if the type-specific link info isn't available. +func (r Info) Cgroup() *CgroupInfo { + e, _ := r.extra.(*CgroupInfo) + return e +} + +// NetNs returns netns type-specific link info. +// +// Returns nil if the type-specific link info isn't available. +func (r Info) NetNs() *NetNsInfo { + e, _ := r.extra.(*NetNsInfo) + return e +} + +// ExtraNetNs returns XDP type-specific link info. +// +// Returns nil if the type-specific link info isn't available. +func (r Info) XDP() *XDPInfo { + e, _ := r.extra.(*XDPInfo) + return e +} + +// RawLink is the low-level API to bpf_link. +// +// You should consider using the higher level interfaces in this +// package instead. +type RawLink struct { + fd *sys.FD + pinnedPath string +} + +// AttachRawLink creates a raw link. +func AttachRawLink(opts RawLinkOptions) (*RawLink, error) { + if err := haveBPFLink(); err != nil { + return nil, err + } + + if opts.Target < 0 { + return nil, fmt.Errorf("invalid target: %s", sys.ErrClosedFd) + } + + progFd := opts.Program.FD() + if progFd < 0 { + return nil, fmt.Errorf("invalid program: %s", sys.ErrClosedFd) + } + + attr := sys.LinkCreateAttr{ + TargetFd: uint32(opts.Target), + ProgFd: uint32(progFd), + AttachType: sys.AttachType(opts.Attach), + TargetBtfId: uint32(opts.BTF), + Flags: opts.Flags, + } + fd, err := sys.LinkCreate(&attr) + if err != nil { + return nil, fmt.Errorf("create link: %w", err) + } + + return &RawLink{fd, ""}, nil +} + +func loadPinnedRawLink(fileName string, opts *ebpf.LoadPinOptions) (*RawLink, error) { + fd, err := sys.ObjGet(&sys.ObjGetAttr{ + Pathname: sys.NewStringPointer(fileName), + FileFlags: opts.Marshal(), + }) + if err != nil { + return nil, fmt.Errorf("load pinned link: %w", err) + } + + return &RawLink{fd, fileName}, nil +} + +func (l *RawLink) isLink() {} + +// FD returns the raw file descriptor. +func (l *RawLink) FD() int { + return l.fd.Int() +} + +// Close breaks the link. +// +// Use Pin if you want to make the link persistent. +func (l *RawLink) Close() error { + return l.fd.Close() +} + +// Pin persists a link past the lifetime of the process. +// +// Calling Close on a pinned Link will not break the link +// until the pin is removed. +func (l *RawLink) Pin(fileName string) error { + if err := internal.Pin(l.pinnedPath, fileName, l.fd); err != nil { + return err + } + l.pinnedPath = fileName + return nil +} + +// Unpin implements the Link interface. +func (l *RawLink) Unpin() error { + if err := internal.Unpin(l.pinnedPath); err != nil { + return err + } + l.pinnedPath = "" + return nil +} + +// IsPinned returns true if the Link has a non-empty pinned path. +func (l *RawLink) IsPinned() bool { + return l.pinnedPath != "" +} + +// Update implements the Link interface. +func (l *RawLink) Update(new *ebpf.Program) error { + return l.UpdateArgs(RawLinkUpdateOptions{ + New: new, + }) +} + +// RawLinkUpdateOptions control the behaviour of RawLink.UpdateArgs. +type RawLinkUpdateOptions struct { + New *ebpf.Program + Old *ebpf.Program + Flags uint32 +} + +// UpdateArgs updates a link based on args. +func (l *RawLink) UpdateArgs(opts RawLinkUpdateOptions) error { + newFd := opts.New.FD() + if newFd < 0 { + return fmt.Errorf("invalid program: %s", sys.ErrClosedFd) + } + + var oldFd int + if opts.Old != nil { + oldFd = opts.Old.FD() + if oldFd < 0 { + return fmt.Errorf("invalid replacement program: %s", sys.ErrClosedFd) + } + } + + attr := sys.LinkUpdateAttr{ + LinkFd: l.fd.Uint(), + NewProgFd: uint32(newFd), + OldProgFd: uint32(oldFd), + Flags: opts.Flags, + } + return sys.LinkUpdate(&attr) +} + +// Info returns metadata about the link. +func (l *RawLink) Info() (*Info, error) { + var info sys.LinkInfo + + if err := sys.ObjInfo(l.fd, &info); err != nil { + return nil, fmt.Errorf("link info: %s", err) + } + + var extra interface{} + switch info.Type { + case CgroupType: + extra = &CgroupInfo{} + case NetNsType: + extra = &NetNsInfo{} + case TracingType: + extra = &TracingInfo{} + case XDPType: + extra = &XDPInfo{} + case RawTracepointType, IterType, + PerfEventType, KprobeMultiType: + // Extra metadata not supported. + default: + return nil, fmt.Errorf("unknown link info type: %d", info.Type) + } + + if extra != nil { + buf := bytes.NewReader(info.Extra[:]) + err := binary.Read(buf, internal.NativeEndian, extra) + if err != nil { + return nil, fmt.Errorf("cannot read extra link info: %w", err) + } + } + + return &Info{ + info.Type, + info.Id, + ebpf.ProgramID(info.ProgId), + extra, + }, nil +} diff --git a/vendor/github.com/cilium/ebpf/link/netns.go b/vendor/github.com/cilium/ebpf/link/netns.go new file mode 100644 index 000000000..344ecced6 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/netns.go @@ -0,0 +1,36 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" +) + +// NetNsLink is a program attached to a network namespace. +type NetNsLink struct { + RawLink +} + +// AttachNetNs attaches a program to a network namespace. +func AttachNetNs(ns int, prog *ebpf.Program) (*NetNsLink, error) { + var attach ebpf.AttachType + switch t := prog.Type(); t { + case ebpf.FlowDissector: + attach = ebpf.AttachFlowDissector + case ebpf.SkLookup: + attach = ebpf.AttachSkLookup + default: + return nil, fmt.Errorf("can't attach %v to network namespace", t) + } + + link, err := AttachRawLink(RawLinkOptions{ + Target: ns, + Program: prog, + Attach: attach, + }) + if err != nil { + return nil, err + } + + return &NetNsLink{*link}, nil +} diff --git a/vendor/github.com/cilium/ebpf/link/perf_event.go b/vendor/github.com/cilium/ebpf/link/perf_event.go new file mode 100644 index 000000000..61f80627a --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/perf_event.go @@ -0,0 +1,434 @@ +package link + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/sys" + "github.com/cilium/ebpf/internal/unix" +) + +// Getting the terminology right is usually the hardest part. For posterity and +// for staying sane during implementation: +// +// - trace event: Representation of a kernel runtime hook. Filesystem entries +// under <tracefs>/events. Can be tracepoints (static), kprobes or uprobes. +// Can be instantiated into perf events (see below). +// - tracepoint: A predetermined hook point in the kernel. Exposed as trace +// events in (sub)directories under <tracefs>/events. Cannot be closed or +// removed, they are static. +// - k(ret)probe: Ephemeral trace events based on entry or exit points of +// exported kernel symbols. kprobe-based (tracefs) trace events can be +// created system-wide by writing to the <tracefs>/kprobe_events file, or +// they can be scoped to the current process by creating PMU perf events. +// - u(ret)probe: Ephemeral trace events based on user provides ELF binaries +// and offsets. uprobe-based (tracefs) trace events can be +// created system-wide by writing to the <tracefs>/uprobe_events file, or +// they can be scoped to the current process by creating PMU perf events. +// - perf event: An object instantiated based on an existing trace event or +// kernel symbol. Referred to by fd in userspace. +// Exactly one eBPF program can be attached to a perf event. Multiple perf +// events can be created from a single trace event. Closing a perf event +// stops any further invocations of the attached eBPF program. + +var ( + tracefsPath = "/sys/kernel/debug/tracing" + + errInvalidInput = errors.New("invalid input") +) + +const ( + perfAllThreads = -1 +) + +type perfEventType uint8 + +const ( + tracepointEvent perfEventType = iota + kprobeEvent + kretprobeEvent + uprobeEvent + uretprobeEvent +) + +// A perfEvent represents a perf event kernel object. Exactly one eBPF program +// can be attached to it. It is created based on a tracefs trace event or a +// Performance Monitoring Unit (PMU). +type perfEvent struct { + // The event type determines the types of programs that can be attached. + typ perfEventType + + // Group and name of the tracepoint/kprobe/uprobe. + group string + name string + + // PMU event ID read from sysfs. Valid IDs are non-zero. + pmuID uint64 + // ID of the trace event read from tracefs. Valid IDs are non-zero. + tracefsID uint64 + + // User provided arbitrary value. + cookie uint64 + + // This is the perf event FD. + fd *sys.FD +} + +func (pe *perfEvent) Close() error { + if err := pe.fd.Close(); err != nil { + return fmt.Errorf("closing perf event fd: %w", err) + } + + switch pe.typ { + case kprobeEvent, kretprobeEvent: + // Clean up kprobe tracefs entry. + if pe.tracefsID != 0 { + return closeTraceFSProbeEvent(kprobeType, pe.group, pe.name) + } + case uprobeEvent, uretprobeEvent: + // Clean up uprobe tracefs entry. + if pe.tracefsID != 0 { + return closeTraceFSProbeEvent(uprobeType, pe.group, pe.name) + } + case tracepointEvent: + // Tracepoint trace events don't hold any extra resources. + return nil + } + + return nil +} + +// perfEventLink represents a bpf perf link. +type perfEventLink struct { + RawLink + pe *perfEvent +} + +func (pl *perfEventLink) isLink() {} + +// Pinning requires the underlying perf event FD to stay open. +// +// | PerfEvent FD | BpfLink FD | Works | +// |--------------|------------|-------| +// | Open | Open | Yes | +// | Closed | Open | No | +// | Open | Closed | No (Pin() -> EINVAL) | +// | Closed | Closed | No (Pin() -> EINVAL) | +// +// There is currently no pretty way to recover the perf event FD +// when loading a pinned link, so leave as not supported for now. +func (pl *perfEventLink) Pin(string) error { + return fmt.Errorf("perf event link pin: %w", ErrNotSupported) +} + +func (pl *perfEventLink) Unpin() error { + return fmt.Errorf("perf event link unpin: %w", ErrNotSupported) +} + +func (pl *perfEventLink) Close() error { + if err := pl.pe.Close(); err != nil { + return fmt.Errorf("perf event link close: %w", err) + } + return pl.fd.Close() +} + +func (pl *perfEventLink) Update(prog *ebpf.Program) error { + return fmt.Errorf("perf event link update: %w", ErrNotSupported) +} + +// perfEventIoctl implements Link and handles the perf event lifecycle +// via ioctl(). +type perfEventIoctl struct { + *perfEvent +} + +func (pi *perfEventIoctl) isLink() {} + +// Since 4.15 (e87c6bc3852b "bpf: permit multiple bpf attachments for a single perf event"), +// calling PERF_EVENT_IOC_SET_BPF appends the given program to a prog_array +// owned by the perf event, which means multiple programs can be attached +// simultaneously. +// +// Before 4.15, calling PERF_EVENT_IOC_SET_BPF more than once on a perf event +// returns EEXIST. +// +// Detaching a program from a perf event is currently not possible, so a +// program replacement mechanism cannot be implemented for perf events. +func (pi *perfEventIoctl) Update(prog *ebpf.Program) error { + return fmt.Errorf("perf event ioctl update: %w", ErrNotSupported) +} + +func (pi *perfEventIoctl) Pin(string) error { + return fmt.Errorf("perf event ioctl pin: %w", ErrNotSupported) +} + +func (pi *perfEventIoctl) Unpin() error { + return fmt.Errorf("perf event ioctl unpin: %w", ErrNotSupported) +} + +func (pi *perfEventIoctl) Info() (*Info, error) { + return nil, fmt.Errorf("perf event ioctl info: %w", ErrNotSupported) +} + +// attach the given eBPF prog to the perf event stored in pe. +// pe must contain a valid perf event fd. +// prog's type must match the program type stored in pe. +func attachPerfEvent(pe *perfEvent, prog *ebpf.Program) (Link, error) { + if prog == nil { + return nil, errors.New("cannot attach a nil program") + } + if prog.FD() < 0 { + return nil, fmt.Errorf("invalid program: %w", sys.ErrClosedFd) + } + + switch pe.typ { + case kprobeEvent, kretprobeEvent, uprobeEvent, uretprobeEvent: + if t := prog.Type(); t != ebpf.Kprobe { + return nil, fmt.Errorf("invalid program type (expected %s): %s", ebpf.Kprobe, t) + } + case tracepointEvent: + if t := prog.Type(); t != ebpf.TracePoint { + return nil, fmt.Errorf("invalid program type (expected %s): %s", ebpf.TracePoint, t) + } + default: + return nil, fmt.Errorf("unknown perf event type: %d", pe.typ) + } + + if err := haveBPFLinkPerfEvent(); err == nil { + return attachPerfEventLink(pe, prog) + } + return attachPerfEventIoctl(pe, prog) +} + +func attachPerfEventIoctl(pe *perfEvent, prog *ebpf.Program) (*perfEventIoctl, error) { + if pe.cookie != 0 { + return nil, fmt.Errorf("cookies are not supported: %w", ErrNotSupported) + } + + // Assign the eBPF program to the perf event. + err := unix.IoctlSetInt(pe.fd.Int(), unix.PERF_EVENT_IOC_SET_BPF, prog.FD()) + if err != nil { + return nil, fmt.Errorf("setting perf event bpf program: %w", err) + } + + // PERF_EVENT_IOC_ENABLE and _DISABLE ignore their given values. + if err := unix.IoctlSetInt(pe.fd.Int(), unix.PERF_EVENT_IOC_ENABLE, 0); err != nil { + return nil, fmt.Errorf("enable perf event: %s", err) + } + + pi := &perfEventIoctl{pe} + + // Close the perf event when its reference is lost to avoid leaking system resources. + runtime.SetFinalizer(pi, (*perfEventIoctl).Close) + return pi, nil +} + +// Use the bpf api to attach the perf event (BPF_LINK_TYPE_PERF_EVENT, 5.15+). +// +// https://github.com/torvalds/linux/commit/b89fbfbb854c9afc3047e8273cc3a694650b802e +func attachPerfEventLink(pe *perfEvent, prog *ebpf.Program) (*perfEventLink, error) { + fd, err := sys.LinkCreatePerfEvent(&sys.LinkCreatePerfEventAttr{ + ProgFd: uint32(prog.FD()), + TargetFd: pe.fd.Uint(), + AttachType: sys.BPF_PERF_EVENT, + BpfCookie: pe.cookie, + }) + if err != nil { + return nil, fmt.Errorf("cannot create bpf perf link: %v", err) + } + + pl := &perfEventLink{RawLink{fd: fd}, pe} + + // Close the perf event when its reference is lost to avoid leaking system resources. + runtime.SetFinalizer(pl, (*perfEventLink).Close) + return pl, nil +} + +// unsafeStringPtr returns an unsafe.Pointer to a NUL-terminated copy of str. +func unsafeStringPtr(str string) (unsafe.Pointer, error) { + p, err := unix.BytePtrFromString(str) + if err != nil { + return nil, err + } + return unsafe.Pointer(p), nil +} + +// getTraceEventID reads a trace event's ID from tracefs given its group and name. +// The kernel requires group and name to be alphanumeric or underscore. +// +// name automatically has its invalid symbols converted to underscores so the caller +// can pass a raw symbol name, e.g. a kernel symbol containing dots. +func getTraceEventID(group, name string) (uint64, error) { + name = sanitizeSymbol(name) + path, err := sanitizePath(tracefsPath, "events", group, name, "id") + if err != nil { + return 0, err + } + tid, err := readUint64FromFile("%d\n", path) + if errors.Is(err, os.ErrNotExist) { + return 0, err + } + if err != nil { + return 0, fmt.Errorf("reading trace event ID of %s/%s: %w", group, name, err) + } + + return tid, nil +} + +// openTracepointPerfEvent opens a tracepoint-type perf event. System-wide +// [k,u]probes created by writing to <tracefs>/[k,u]probe_events are tracepoints +// behind the scenes, and can be attached to using these perf events. +func openTracepointPerfEvent(tid uint64, pid int) (*sys.FD, error) { + attr := unix.PerfEventAttr{ + Type: unix.PERF_TYPE_TRACEPOINT, + Config: tid, + Sample_type: unix.PERF_SAMPLE_RAW, + Sample: 1, + Wakeup: 1, + } + + fd, err := unix.PerfEventOpen(&attr, pid, 0, -1, unix.PERF_FLAG_FD_CLOEXEC) + if err != nil { + return nil, fmt.Errorf("opening tracepoint perf event: %w", err) + } + + return sys.NewFD(fd) +} + +func sanitizePath(base string, path ...string) (string, error) { + l := filepath.Join(path...) + p := filepath.Join(base, l) + if !strings.HasPrefix(p, base) { + return "", fmt.Errorf("path '%s' attempts to escape base path '%s': %w", l, base, errInvalidInput) + } + return p, nil +} + +// readUint64FromFile reads a uint64 from a file. +// +// format specifies the contents of the file in fmt.Scanf syntax. +func readUint64FromFile(format string, path ...string) (uint64, error) { + filename := filepath.Join(path...) + data, err := os.ReadFile(filename) + if err != nil { + return 0, fmt.Errorf("reading file %q: %w", filename, err) + } + + var value uint64 + n, err := fmt.Fscanf(bytes.NewReader(data), format, &value) + if err != nil { + return 0, fmt.Errorf("parsing file %q: %w", filename, err) + } + if n != 1 { + return 0, fmt.Errorf("parsing file %q: expected 1 item, got %d", filename, n) + } + + return value, nil +} + +type uint64FromFileKey struct { + format, path string +} + +var uint64FromFileCache = struct { + sync.RWMutex + values map[uint64FromFileKey]uint64 +}{ + values: map[uint64FromFileKey]uint64{}, +} + +// readUint64FromFileOnce is like readUint64FromFile but memoizes the result. +func readUint64FromFileOnce(format string, path ...string) (uint64, error) { + filename := filepath.Join(path...) + key := uint64FromFileKey{format, filename} + + uint64FromFileCache.RLock() + if value, ok := uint64FromFileCache.values[key]; ok { + uint64FromFileCache.RUnlock() + return value, nil + } + uint64FromFileCache.RUnlock() + + value, err := readUint64FromFile(format, filename) + if err != nil { + return 0, err + } + + uint64FromFileCache.Lock() + defer uint64FromFileCache.Unlock() + + if value, ok := uint64FromFileCache.values[key]; ok { + // Someone else got here before us, use what is cached. + return value, nil + } + + uint64FromFileCache.values[key] = value + return value, nil +} + +// Probe BPF perf link. +// +// https://elixir.bootlin.com/linux/v5.16.8/source/kernel/bpf/syscall.c#L4307 +// https://github.com/torvalds/linux/commit/b89fbfbb854c9afc3047e8273cc3a694650b802e +var haveBPFLinkPerfEvent = internal.NewFeatureTest("bpf_link_perf_event", "5.15", func() error { + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Name: "probe_bpf_perf_link", + Type: ebpf.Kprobe, + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + License: "MIT", + }) + if err != nil { + return err + } + defer prog.Close() + + _, err = sys.LinkCreatePerfEvent(&sys.LinkCreatePerfEventAttr{ + ProgFd: uint32(prog.FD()), + AttachType: sys.BPF_PERF_EVENT, + }) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) + +// isValidTraceID implements the equivalent of a regex match +// against "^[a-zA-Z_][0-9a-zA-Z_]*$". +// +// Trace event groups, names and kernel symbols must adhere to this set +// of characters. Non-empty, first character must not be a number, all +// characters must be alphanumeric or underscore. +func isValidTraceID(s string) bool { + if len(s) < 1 { + return false + } + for i, c := range []byte(s) { + switch { + case c >= 'a' && c <= 'z': + case c >= 'A' && c <= 'Z': + case c == '_': + case i > 0 && c >= '0' && c <= '9': + + default: + return false + } + } + + return true +} diff --git a/vendor/github.com/cilium/ebpf/link/platform.go b/vendor/github.com/cilium/ebpf/link/platform.go new file mode 100644 index 000000000..eb6f7b7a3 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/platform.go @@ -0,0 +1,25 @@ +package link + +import ( + "fmt" + "runtime" +) + +func platformPrefix(symbol string) string { + + prefix := runtime.GOARCH + + // per https://github.com/golang/go/blob/master/src/go/build/syslist.go + switch prefix { + case "386": + prefix = "ia32" + case "amd64", "amd64p32": + prefix = "x64" + case "arm64", "arm64be": + prefix = "arm64" + default: + return symbol + } + + return fmt.Sprintf("__%s_%s", prefix, symbol) +} diff --git a/vendor/github.com/cilium/ebpf/link/program.go b/vendor/github.com/cilium/ebpf/link/program.go new file mode 100644 index 000000000..ea3181737 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/program.go @@ -0,0 +1,76 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/sys" +) + +type RawAttachProgramOptions struct { + // File descriptor to attach to. This differs for each attach type. + Target int + // Program to attach. + Program *ebpf.Program + // Program to replace (cgroups). + Replace *ebpf.Program + // Attach must match the attach type of Program (and Replace). + Attach ebpf.AttachType + // Flags control the attach behaviour. This differs for each attach type. + Flags uint32 +} + +// RawAttachProgram is a low level wrapper around BPF_PROG_ATTACH. +// +// You should use one of the higher level abstractions available in this +// package if possible. +func RawAttachProgram(opts RawAttachProgramOptions) error { + if err := haveProgAttach(); err != nil { + return err + } + + var replaceFd uint32 + if opts.Replace != nil { + replaceFd = uint32(opts.Replace.FD()) + } + + attr := sys.ProgAttachAttr{ + TargetFd: uint32(opts.Target), + AttachBpfFd: uint32(opts.Program.FD()), + ReplaceBpfFd: replaceFd, + AttachType: uint32(opts.Attach), + AttachFlags: uint32(opts.Flags), + } + + if err := sys.ProgAttach(&attr); err != nil { + return fmt.Errorf("can't attach program: %w", err) + } + return nil +} + +type RawDetachProgramOptions struct { + Target int + Program *ebpf.Program + Attach ebpf.AttachType +} + +// RawDetachProgram is a low level wrapper around BPF_PROG_DETACH. +// +// You should use one of the higher level abstractions available in this +// package if possible. +func RawDetachProgram(opts RawDetachProgramOptions) error { + if err := haveProgAttach(); err != nil { + return err + } + + attr := sys.ProgDetachAttr{ + TargetFd: uint32(opts.Target), + AttachBpfFd: uint32(opts.Program.FD()), + AttachType: uint32(opts.Attach), + } + if err := sys.ProgDetach(&attr); err != nil { + return fmt.Errorf("can't detach program: %w", err) + } + + return nil +} diff --git a/vendor/github.com/cilium/ebpf/link/query.go b/vendor/github.com/cilium/ebpf/link/query.go new file mode 100644 index 000000000..8c882414d --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/query.go @@ -0,0 +1,63 @@ +package link + +import ( + "fmt" + "os" + "unsafe" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/sys" +) + +// QueryOptions defines additional parameters when querying for programs. +type QueryOptions struct { + // Path can be a path to a cgroup, netns or LIRC2 device + Path string + // Attach specifies the AttachType of the programs queried for + Attach ebpf.AttachType + // QueryFlags are flags for BPF_PROG_QUERY, e.g. BPF_F_QUERY_EFFECTIVE + QueryFlags uint32 +} + +// QueryPrograms retrieves ProgramIDs associated with the AttachType. +// +// It only returns IDs of programs that were attached using PROG_ATTACH and not bpf_link. +// Returns (nil, nil) if there are no programs attached to the queried kernel resource. +// Calling QueryPrograms on a kernel missing PROG_QUERY will result in ErrNotSupported. +func QueryPrograms(opts QueryOptions) ([]ebpf.ProgramID, error) { + if haveProgQuery() != nil { + return nil, fmt.Errorf("can't query program IDs: %w", ErrNotSupported) + } + + f, err := os.Open(opts.Path) + if err != nil { + return nil, fmt.Errorf("can't open file: %s", err) + } + defer f.Close() + + // query the number of programs to allocate correct slice size + attr := sys.ProgQueryAttr{ + TargetFd: uint32(f.Fd()), + AttachType: sys.AttachType(opts.Attach), + QueryFlags: opts.QueryFlags, + } + if err := sys.ProgQuery(&attr); err != nil { + return nil, fmt.Errorf("can't query program count: %w", err) + } + + // return nil if no progs are attached + if attr.ProgCount == 0 { + return nil, nil + } + + // we have at least one prog, so we query again + progIds := make([]ebpf.ProgramID, attr.ProgCount) + attr.ProgIds = sys.NewPointer(unsafe.Pointer(&progIds[0])) + attr.ProgCount = uint32(len(progIds)) + if err := sys.ProgQuery(&attr); err != nil { + return nil, fmt.Errorf("can't query program IDs: %w", err) + } + + return progIds, nil + +} diff --git a/vendor/github.com/cilium/ebpf/link/raw_tracepoint.go b/vendor/github.com/cilium/ebpf/link/raw_tracepoint.go new file mode 100644 index 000000000..925e621cb --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/raw_tracepoint.go @@ -0,0 +1,87 @@ +package link + +import ( + "errors" + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/sys" +) + +type RawTracepointOptions struct { + // Tracepoint name. + Name string + // Program must be of type RawTracepoint* + Program *ebpf.Program +} + +// AttachRawTracepoint links a BPF program to a raw_tracepoint. +// +// Requires at least Linux 4.17. +func AttachRawTracepoint(opts RawTracepointOptions) (Link, error) { + if t := opts.Program.Type(); t != ebpf.RawTracepoint && t != ebpf.RawTracepointWritable { + return nil, fmt.Errorf("invalid program type %s, expected RawTracepoint(Writable)", t) + } + if opts.Program.FD() < 0 { + return nil, fmt.Errorf("invalid program: %w", sys.ErrClosedFd) + } + + fd, err := sys.RawTracepointOpen(&sys.RawTracepointOpenAttr{ + Name: sys.NewStringPointer(opts.Name), + ProgFd: uint32(opts.Program.FD()), + }) + if err != nil { + return nil, err + } + + err = haveBPFLink() + if errors.Is(err, ErrNotSupported) { + // Prior to commit 70ed506c3bbc ("bpf: Introduce pinnable bpf_link abstraction") + // raw_tracepoints are just a plain fd. + return &simpleRawTracepoint{fd}, nil + } + + if err != nil { + return nil, err + } + + return &rawTracepoint{RawLink{fd: fd}}, nil +} + +type simpleRawTracepoint struct { + fd *sys.FD +} + +var _ Link = (*simpleRawTracepoint)(nil) + +func (frt *simpleRawTracepoint) isLink() {} + +func (frt *simpleRawTracepoint) Close() error { + return frt.fd.Close() +} + +func (frt *simpleRawTracepoint) Update(_ *ebpf.Program) error { + return fmt.Errorf("update raw_tracepoint: %w", ErrNotSupported) +} + +func (frt *simpleRawTracepoint) Pin(string) error { + return fmt.Errorf("pin raw_tracepoint: %w", ErrNotSupported) +} + +func (frt *simpleRawTracepoint) Unpin() error { + return fmt.Errorf("unpin raw_tracepoint: %w", ErrNotSupported) +} + +func (frt *simpleRawTracepoint) Info() (*Info, error) { + return nil, fmt.Errorf("can't get raw_tracepoint info: %w", ErrNotSupported) +} + +type rawTracepoint struct { + RawLink +} + +var _ Link = (*rawTracepoint)(nil) + +func (rt *rawTracepoint) Update(_ *ebpf.Program) error { + return fmt.Errorf("update raw_tracepoint: %w", ErrNotSupported) +} diff --git a/vendor/github.com/cilium/ebpf/link/socket_filter.go b/vendor/github.com/cilium/ebpf/link/socket_filter.go new file mode 100644 index 000000000..94f3958cc --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/socket_filter.go @@ -0,0 +1,40 @@ +package link + +import ( + "syscall" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal/unix" +) + +// AttachSocketFilter attaches a SocketFilter BPF program to a socket. +func AttachSocketFilter(conn syscall.Conn, program *ebpf.Program) error { + rawConn, err := conn.SyscallConn() + if err != nil { + return err + } + var ssoErr error + err = rawConn.Control(func(fd uintptr) { + ssoErr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_ATTACH_BPF, program.FD()) + }) + if ssoErr != nil { + return ssoErr + } + return err +} + +// DetachSocketFilter detaches a SocketFilter BPF program from a socket. +func DetachSocketFilter(conn syscall.Conn) error { + rawConn, err := conn.SyscallConn() + if err != nil { + return err + } + var ssoErr error + err = rawConn.Control(func(fd uintptr) { + ssoErr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_DETACH_BPF, 0) + }) + if ssoErr != nil { + return ssoErr + } + return err +} diff --git a/vendor/github.com/cilium/ebpf/link/syscalls.go b/vendor/github.com/cilium/ebpf/link/syscalls.go new file mode 100644 index 000000000..38f7ae9b7 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/syscalls.go @@ -0,0 +1,123 @@ +package link + +import ( + "errors" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/asm" + "github.com/cilium/ebpf/internal" + "github.com/cilium/ebpf/internal/sys" + "github.com/cilium/ebpf/internal/unix" +) + +// Type is the kind of link. +type Type = sys.LinkType + +// Valid link types. +const ( + UnspecifiedType = sys.BPF_LINK_TYPE_UNSPEC + RawTracepointType = sys.BPF_LINK_TYPE_RAW_TRACEPOINT + TracingType = sys.BPF_LINK_TYPE_TRACING + CgroupType = sys.BPF_LINK_TYPE_CGROUP + IterType = sys.BPF_LINK_TYPE_ITER + NetNsType = sys.BPF_LINK_TYPE_NETNS + XDPType = sys.BPF_LINK_TYPE_XDP + PerfEventType = sys.BPF_LINK_TYPE_PERF_EVENT + KprobeMultiType = sys.BPF_LINK_TYPE_KPROBE_MULTI +) + +var haveProgAttach = internal.NewFeatureTest("BPF_PROG_ATTACH", "4.10", func() error { + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.CGroupSKB, + License: "MIT", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }) + if err != nil { + return internal.ErrNotSupported + } + + // BPF_PROG_ATTACH was introduced at the same time as CGgroupSKB, + // so being able to load the program is enough to infer that we + // have the syscall. + prog.Close() + return nil +}) + +var haveProgAttachReplace = internal.NewFeatureTest("BPF_PROG_ATTACH atomic replacement", "5.5", func() error { + if err := haveProgAttach(); err != nil { + return err + } + + prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{ + Type: ebpf.CGroupSKB, + AttachType: ebpf.AttachCGroupInetIngress, + License: "MIT", + Instructions: asm.Instructions{ + asm.Mov.Imm(asm.R0, 0), + asm.Return(), + }, + }) + if err != nil { + return internal.ErrNotSupported + } + defer prog.Close() + + // We know that we have BPF_PROG_ATTACH since we can load CGroupSKB programs. + // If passing BPF_F_REPLACE gives us EINVAL we know that the feature isn't + // present. + attr := sys.ProgAttachAttr{ + // We rely on this being checked after attachFlags. + TargetFd: ^uint32(0), + AttachBpfFd: uint32(prog.FD()), + AttachType: uint32(ebpf.AttachCGroupInetIngress), + AttachFlags: uint32(flagReplace), + } + + err = sys.ProgAttach(&attr) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) + +var haveBPFLink = internal.NewFeatureTest("bpf_link", "5.7", func() error { + attr := sys.LinkCreateAttr{ + // This is a hopefully invalid file descriptor, which triggers EBADF. + TargetFd: ^uint32(0), + ProgFd: ^uint32(0), + AttachType: sys.AttachType(ebpf.AttachCGroupInetIngress), + } + _, err := sys.LinkCreate(&attr) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) + +var haveProgQuery = internal.NewFeatureTest("BPF_PROG_QUERY", "4.15", func() error { + attr := sys.ProgQueryAttr{ + // We rely on this being checked during the syscall. + // With an otherwise correct payload we expect EBADF here + // as an indication that the feature is present. + TargetFd: ^uint32(0), + AttachType: sys.AttachType(ebpf.AttachCGroupInetIngress), + } + + err := sys.ProgQuery(&attr) + if errors.Is(err, unix.EINVAL) { + return internal.ErrNotSupported + } + if errors.Is(err, unix.EBADF) { + return nil + } + return err +}) diff --git a/vendor/github.com/cilium/ebpf/link/tracepoint.go b/vendor/github.com/cilium/ebpf/link/tracepoint.go new file mode 100644 index 000000000..a59ef9d1c --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/tracepoint.go @@ -0,0 +1,77 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" +) + +// TracepointOptions defines additional parameters that will be used +// when loading Tracepoints. +type TracepointOptions struct { + // Arbitrary value that can be fetched from an eBPF program + // via `bpf_get_attach_cookie()`. + // + // Needs kernel 5.15+. + Cookie uint64 +} + +// Tracepoint attaches the given eBPF program to the tracepoint with the given +// group and name. See /sys/kernel/debug/tracing/events to find available +// tracepoints. The top-level directory is the group, the event's subdirectory +// is the name. Example: +// +// tp, err := Tracepoint("syscalls", "sys_enter_fork", prog, nil) +// +// Losing the reference to the resulting Link (tp) will close the Tracepoint +// and prevent further execution of prog. The Link must be Closed during +// program shutdown to avoid leaking system resources. +// +// Note that attaching eBPF programs to syscalls (sys_enter_*/sys_exit_*) is +// only possible as of kernel 4.14 (commit cf5f5ce). +func Tracepoint(group, name string, prog *ebpf.Program, opts *TracepointOptions) (Link, error) { + if group == "" || name == "" { + return nil, fmt.Errorf("group and name cannot be empty: %w", errInvalidInput) + } + if prog == nil { + return nil, fmt.Errorf("prog cannot be nil: %w", errInvalidInput) + } + if !isValidTraceID(group) || !isValidTraceID(name) { + return nil, fmt.Errorf("group and name '%s/%s' must be alphanumeric or underscore: %w", group, name, errInvalidInput) + } + if prog.Type() != ebpf.TracePoint { + return nil, fmt.Errorf("eBPF program type %s is not a Tracepoint: %w", prog.Type(), errInvalidInput) + } + + tid, err := getTraceEventID(group, name) + if err != nil { + return nil, err + } + + fd, err := openTracepointPerfEvent(tid, perfAllThreads) + if err != nil { + return nil, err + } + + var cookie uint64 + if opts != nil { + cookie = opts.Cookie + } + + pe := &perfEvent{ + typ: tracepointEvent, + group: group, + name: name, + tracefsID: tid, + cookie: cookie, + fd: fd, + } + + lnk, err := attachPerfEvent(pe, prog) + if err != nil { + pe.Close() + return nil, err + } + + return lnk, nil +} diff --git a/vendor/github.com/cilium/ebpf/link/tracing.go b/vendor/github.com/cilium/ebpf/link/tracing.go new file mode 100644 index 000000000..e26cc9149 --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/tracing.go @@ -0,0 +1,150 @@ +package link + +import ( + "errors" + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/btf" + "github.com/cilium/ebpf/internal/sys" +) + +type tracing struct { + RawLink +} + +func (f *tracing) Update(new *ebpf.Program) error { + return fmt.Errorf("tracing update: %w", ErrNotSupported) +} + +// AttachFreplace attaches the given eBPF program to the function it replaces. +// +// The program and name can either be provided at link time, or can be provided +// at program load time. If they were provided at load time, they should be nil +// and empty respectively here, as they will be ignored by the kernel. +// Examples: +// +// AttachFreplace(dispatcher, "function", replacement) +// AttachFreplace(nil, "", replacement) +func AttachFreplace(targetProg *ebpf.Program, name string, prog *ebpf.Program) (Link, error) { + if (name == "") != (targetProg == nil) { + return nil, fmt.Errorf("must provide both or neither of name and targetProg: %w", errInvalidInput) + } + if prog == nil { + return nil, fmt.Errorf("prog cannot be nil: %w", errInvalidInput) + } + if prog.Type() != ebpf.Extension { + return nil, fmt.Errorf("eBPF program type %s is not an Extension: %w", prog.Type(), errInvalidInput) + } + + var ( + target int + typeID btf.TypeID + ) + if targetProg != nil { + btfHandle, err := targetProg.Handle() + if err != nil { + return nil, err + } + defer btfHandle.Close() + + spec, err := btfHandle.Spec() + if err != nil { + return nil, err + } + + var function *btf.Func + if err := spec.TypeByName(name, &function); err != nil { + return nil, err + } + + target = targetProg.FD() + typeID, err = spec.TypeID(function) + if err != nil { + return nil, err + } + } + + link, err := AttachRawLink(RawLinkOptions{ + Target: target, + Program: prog, + Attach: ebpf.AttachNone, + BTF: typeID, + }) + if errors.Is(err, sys.ENOTSUPP) { + // This may be returned by bpf_tracing_prog_attach via bpf_arch_text_poke. + return nil, fmt.Errorf("create raw tracepoint: %w", ErrNotSupported) + } + if err != nil { + return nil, err + } + + return &tracing{*link}, nil +} + +type TracingOptions struct { + // Program must be of type Tracing with attach type + // AttachTraceFEntry/AttachTraceFExit/AttachModifyReturn or + // AttachTraceRawTp. + Program *ebpf.Program +} + +type LSMOptions struct { + // Program must be of type LSM with attach type + // AttachLSMMac. + Program *ebpf.Program +} + +// attachBTFID links all BPF program types (Tracing/LSM) that they attach to a btf_id. +func attachBTFID(program *ebpf.Program) (Link, error) { + if program.FD() < 0 { + return nil, fmt.Errorf("invalid program %w", sys.ErrClosedFd) + } + + fd, err := sys.RawTracepointOpen(&sys.RawTracepointOpenAttr{ + ProgFd: uint32(program.FD()), + }) + if errors.Is(err, sys.ENOTSUPP) { + // This may be returned by bpf_tracing_prog_attach via bpf_arch_text_poke. + return nil, fmt.Errorf("create raw tracepoint: %w", ErrNotSupported) + } + if err != nil { + return nil, fmt.Errorf("create raw tracepoint: %w", err) + } + + raw := RawLink{fd: fd} + info, err := raw.Info() + if err != nil { + raw.Close() + return nil, err + } + + if info.Type == RawTracepointType { + // Sadness upon sadness: a Tracing program with AttachRawTp returns + // a raw_tracepoint link. Other types return a tracing link. + return &rawTracepoint{raw}, nil + } + + return &tracing{RawLink: RawLink{fd: fd}}, nil +} + +// AttachTracing links a tracing (fentry/fexit/fmod_ret) BPF program or +// a BTF-powered raw tracepoint (tp_btf) BPF Program to a BPF hook defined +// in kernel modules. +func AttachTracing(opts TracingOptions) (Link, error) { + if t := opts.Program.Type(); t != ebpf.Tracing { + return nil, fmt.Errorf("invalid program type %s, expected Tracing", t) + } + + return attachBTFID(opts.Program) +} + +// AttachLSM links a Linux security module (LSM) BPF Program to a BPF +// hook defined in kernel modules. +func AttachLSM(opts LSMOptions) (Link, error) { + if t := opts.Program.Type(); t != ebpf.LSM { + return nil, fmt.Errorf("invalid program type %s, expected LSM", t) + } + + return attachBTFID(opts.Program) +} diff --git a/vendor/github.com/cilium/ebpf/link/uprobe.go b/vendor/github.com/cilium/ebpf/link/uprobe.go new file mode 100644 index 000000000..aa1ad9bbe --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/uprobe.go @@ -0,0 +1,359 @@ +package link + +import ( + "debug/elf" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/internal" +) + +var ( + uprobeEventsPath = filepath.Join(tracefsPath, "uprobe_events") + + uprobeRefCtrOffsetPMUPath = "/sys/bus/event_source/devices/uprobe/format/ref_ctr_offset" + // elixir.bootlin.com/linux/v5.15-rc7/source/kernel/events/core.c#L9799 + uprobeRefCtrOffsetShift = 32 + haveRefCtrOffsetPMU = internal.NewFeatureTest("RefCtrOffsetPMU", "4.20", func() error { + _, err := os.Stat(uprobeRefCtrOffsetPMUPath) + if err != nil { + return internal.ErrNotSupported + } + return nil + }) + + // ErrNoSymbol indicates that the given symbol was not found + // in the ELF symbols table. + ErrNoSymbol = errors.New("not found") +) + +// Executable defines an executable program on the filesystem. +type Executable struct { + // Path of the executable on the filesystem. + path string + // Parsed ELF and dynamic symbols' addresses. + addresses map[string]uint64 +} + +// UprobeOptions defines additional parameters that will be used +// when loading Uprobes. +type UprobeOptions struct { + // Symbol address. Must be provided in case of external symbols (shared libs). + // If set, overrides the address eventually parsed from the executable. + Address uint64 + // The offset relative to given symbol. Useful when tracing an arbitrary point + // inside the frame of given symbol. + // + // Note: this field changed from being an absolute offset to being relative + // to Address. + Offset uint64 + // Only set the uprobe on the given process ID. Useful when tracing + // shared library calls or programs that have many running instances. + PID int + // Automatically manage SDT reference counts (semaphores). + // + // If this field is set, the Kernel will increment/decrement the + // semaphore located in the process memory at the provided address on + // probe attach/detach. + // + // See also: + // sourceware.org/systemtap/wiki/UserSpaceProbeImplementation (Semaphore Handling) + // github.com/torvalds/linux/commit/1cc33161a83d + // github.com/torvalds/linux/commit/a6ca88b241d5 + RefCtrOffset uint64 + // Arbitrary value that can be fetched from an eBPF program + // via `bpf_get_attach_cookie()`. + // + // Needs kernel 5.15+. + Cookie uint64 +} + +// To open a new Executable, use: +// +// OpenExecutable("/bin/bash") +// +// The returned value can then be used to open Uprobe(s). +func OpenExecutable(path string) (*Executable, error) { + if path == "" { + return nil, fmt.Errorf("path cannot be empty") + } + + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open file '%s': %w", path, err) + } + defer f.Close() + + se, err := internal.NewSafeELFFile(f) + if err != nil { + return nil, fmt.Errorf("parse ELF file: %w", err) + } + + if se.Type != elf.ET_EXEC && se.Type != elf.ET_DYN { + // ELF is not an executable or a shared object. + return nil, errors.New("the given file is not an executable or a shared object") + } + + ex := Executable{ + path: path, + addresses: make(map[string]uint64), + } + + if err := ex.load(se); err != nil { + return nil, err + } + + return &ex, nil +} + +func (ex *Executable) load(f *internal.SafeELFFile) error { + syms, err := f.Symbols() + if err != nil && !errors.Is(err, elf.ErrNoSymbols) { + return err + } + + dynsyms, err := f.DynamicSymbols() + if err != nil && !errors.Is(err, elf.ErrNoSymbols) { + return err + } + + syms = append(syms, dynsyms...) + + for _, s := range syms { + if elf.ST_TYPE(s.Info) != elf.STT_FUNC { + // Symbol not associated with a function or other executable code. + continue + } + + address := s.Value + + // Loop over ELF segments. + for _, prog := range f.Progs { + // Skip uninteresting segments. + if prog.Type != elf.PT_LOAD || (prog.Flags&elf.PF_X) == 0 { + continue + } + + if prog.Vaddr <= s.Value && s.Value < (prog.Vaddr+prog.Memsz) { + // If the symbol value is contained in the segment, calculate + // the symbol offset. + // + // fn symbol offset = fn symbol VA - .text VA + .text offset + // + // stackoverflow.com/a/40249502 + address = s.Value - prog.Vaddr + prog.Off + break + } + } + + ex.addresses[s.Name] = address + } + + return nil +} + +// address calculates the address of a symbol in the executable. +// +// opts must not be nil. +func (ex *Executable) address(symbol string, opts *UprobeOptions) (uint64, error) { + if opts.Address > 0 { + return opts.Address + opts.Offset, nil + } + + address, ok := ex.addresses[symbol] + if !ok { + return 0, fmt.Errorf("symbol %s: %w", symbol, ErrNoSymbol) + } + + // Symbols with location 0 from section undef are shared library calls and + // are relocated before the binary is executed. Dynamic linking is not + // implemented by the library, so mark this as unsupported for now. + // + // Since only offset values are stored and not elf.Symbol, if the value is 0, + // assume it's an external symbol. + if address == 0 { + return 0, fmt.Errorf("cannot resolve %s library call '%s': %w "+ + "(consider providing UprobeOptions.Address)", ex.path, symbol, ErrNotSupported) + } + + return address + opts.Offset, nil +} + +// Uprobe attaches the given eBPF program to a perf event that fires when the +// given symbol starts executing in the given Executable. +// For example, /bin/bash::main(): +// +// ex, _ = OpenExecutable("/bin/bash") +// ex.Uprobe("main", prog, nil) +// +// When using symbols which belongs to shared libraries, +// an offset must be provided via options: +// +// up, err := ex.Uprobe("main", prog, &UprobeOptions{Offset: 0x123}) +// +// Note: Setting the Offset field in the options supersedes the symbol's offset. +// +// Losing the reference to the resulting Link (up) will close the Uprobe +// and prevent further execution of prog. The Link must be Closed during +// program shutdown to avoid leaking system resources. +// +// Functions provided by shared libraries can currently not be traced and +// will result in an ErrNotSupported. +func (ex *Executable) Uprobe(symbol string, prog *ebpf.Program, opts *UprobeOptions) (Link, error) { + u, err := ex.uprobe(symbol, prog, opts, false) + if err != nil { + return nil, err + } + + lnk, err := attachPerfEvent(u, prog) + if err != nil { + u.Close() + return nil, err + } + + return lnk, nil +} + +// Uretprobe attaches the given eBPF program to a perf event that fires right +// before the given symbol exits. For example, /bin/bash::main(): +// +// ex, _ = OpenExecutable("/bin/bash") +// ex.Uretprobe("main", prog, nil) +// +// When using symbols which belongs to shared libraries, +// an offset must be provided via options: +// +// up, err := ex.Uretprobe("main", prog, &UprobeOptions{Offset: 0x123}) +// +// Note: Setting the Offset field in the options supersedes the symbol's offset. +// +// Losing the reference to the resulting Link (up) will close the Uprobe +// and prevent further execution of prog. The Link must be Closed during +// program shutdown to avoid leaking system resources. +// +// Functions provided by shared libraries can currently not be traced and +// will result in an ErrNotSupported. +func (ex *Executable) Uretprobe(symbol string, prog *ebpf.Program, opts *UprobeOptions) (Link, error) { + u, err := ex.uprobe(symbol, prog, opts, true) + if err != nil { + return nil, err + } + + lnk, err := attachPerfEvent(u, prog) + if err != nil { + u.Close() + return nil, err + } + + return lnk, nil +} + +// uprobe opens a perf event for the given binary/symbol and attaches prog to it. +// If ret is true, create a uretprobe. +func (ex *Executable) uprobe(symbol string, prog *ebpf.Program, opts *UprobeOptions, ret bool) (*perfEvent, error) { + if prog == nil { + return nil, fmt.Errorf("prog cannot be nil: %w", errInvalidInput) + } + if prog.Type() != ebpf.Kprobe { + return nil, fmt.Errorf("eBPF program type %s is not Kprobe: %w", prog.Type(), errInvalidInput) + } + if opts == nil { + opts = &UprobeOptions{} + } + + offset, err := ex.address(symbol, opts) + if err != nil { + return nil, err + } + + pid := opts.PID + if pid == 0 { + pid = perfAllThreads + } + + if opts.RefCtrOffset != 0 { + if err := haveRefCtrOffsetPMU(); err != nil { + return nil, fmt.Errorf("uprobe ref_ctr_offset: %w", err) + } + } + + args := probeArgs{ + symbol: symbol, + path: ex.path, + offset: offset, + pid: pid, + refCtrOffset: opts.RefCtrOffset, + ret: ret, + cookie: opts.Cookie, + } + + // Use uprobe PMU if the kernel has it available. + tp, err := pmuUprobe(args) + if err == nil { + return tp, nil + } + if err != nil && !errors.Is(err, ErrNotSupported) { + return nil, fmt.Errorf("creating perf_uprobe PMU: %w", err) + } + + // Use tracefs if uprobe PMU is missing. + args.symbol = sanitizeSymbol(symbol) + tp, err = tracefsUprobe(args) + if err != nil { + return nil, fmt.Errorf("creating trace event '%s:%s' in tracefs: %w", ex.path, symbol, err) + } + + return tp, nil +} + +// pmuUprobe opens a perf event based on the uprobe PMU. +func pmuUprobe(args probeArgs) (*perfEvent, error) { + return pmuProbe(uprobeType, args) +} + +// tracefsUprobe creates a Uprobe tracefs entry. +func tracefsUprobe(args probeArgs) (*perfEvent, error) { + return tracefsProbe(uprobeType, args) +} + +// sanitizeSymbol replaces every invalid character for the tracefs api with an underscore. +// It is equivalent to calling regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString("_"). +func sanitizeSymbol(s string) string { + var b strings.Builder + b.Grow(len(s)) + var skip bool + for _, c := range []byte(s) { + switch { + case c >= 'a' && c <= 'z', + c >= 'A' && c <= 'Z', + c >= '0' && c <= '9': + skip = false + b.WriteByte(c) + + default: + if !skip { + b.WriteByte('_') + skip = true + } + } + } + + return b.String() +} + +// uprobeToken creates the PATH:OFFSET(REF_CTR_OFFSET) token for the tracefs api. +func uprobeToken(args probeArgs) string { + po := fmt.Sprintf("%s:%#x", args.path, args.offset) + + if args.refCtrOffset != 0 { + // This is not documented in Documentation/trace/uprobetracer.txt. + // elixir.bootlin.com/linux/v5.15-rc7/source/kernel/trace/trace.c#L5564 + po += fmt.Sprintf("(%#x)", args.refCtrOffset) + } + + return po +} diff --git a/vendor/github.com/cilium/ebpf/link/xdp.go b/vendor/github.com/cilium/ebpf/link/xdp.go new file mode 100644 index 000000000..aa8dd3a4c --- /dev/null +++ b/vendor/github.com/cilium/ebpf/link/xdp.go @@ -0,0 +1,54 @@ +package link + +import ( + "fmt" + + "github.com/cilium/ebpf" +) + +// XDPAttachFlags represents how XDP program will be attached to interface. +type XDPAttachFlags uint32 + +const ( + // XDPGenericMode (SKB) links XDP BPF program for drivers which do + // not yet support native XDP. + XDPGenericMode XDPAttachFlags = 1 << (iota + 1) + // XDPDriverMode links XDP BPF program into the driver’s receive path. + XDPDriverMode + // XDPOffloadMode offloads the entire XDP BPF program into hardware. + XDPOffloadMode +) + +type XDPOptions struct { + // Program must be an XDP BPF program. + Program *ebpf.Program + + // Interface is the interface index to attach program to. + Interface int + + // Flags is one of XDPAttachFlags (optional). + // + // Only one XDP mode should be set, without flag defaults + // to driver/generic mode (best effort). + Flags XDPAttachFlags +} + +// AttachXDP links an XDP BPF program to an XDP hook. +func AttachXDP(opts XDPOptions) (Link, error) { + if t := opts.Program.Type(); t != ebpf.XDP { + return nil, fmt.Errorf("invalid program type %s, expected XDP", t) + } + + if opts.Interface < 1 { + return nil, fmt.Errorf("invalid interface index: %d", opts.Interface) + } + + rawLink, err := AttachRawLink(RawLinkOptions{ + Program: opts.Program, + Attach: ebpf.AttachXDP, + Target: opts.Interface, + Flags: uint32(opts.Flags), + }) + + return rawLink, err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2d580fafb..f7eac7119 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -16,6 +16,7 @@ github.com/cilium/ebpf/internal github.com/cilium/ebpf/internal/epoll github.com/cilium/ebpf/internal/sys github.com/cilium/ebpf/internal/unix +github.com/cilium/ebpf/link github.com/cilium/ebpf/ringbuf github.com/cilium/ebpf/rlimit # github.com/davecgh/go-spew v1.1.1 -- GitLab