From 872c9b9304c7f556280a94ed52fa4d7fb2afc52c Mon Sep 17 00:00:00 2001 From: Vincent Li Date: Fri, 6 Sep 2024 18:25:41 +0000 Subject: [PATCH] Add xdp-dns for domain blocking use eBPF hash map for domain name lookup Signed-off-by: Vincent Li --- Makefile | 2 +- xdp-dns/Makefile | 9 ++ xdp-dns/xdp_dns.bpf.c | 277 ++++++++++++++++++++++++++++++++++++++++++ xdp-dns/xdp_dns.c | 66 ++++++++++ xdp-dns/xdp_dns.h | 91 ++++++++++++++ 5 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 xdp-dns/Makefile create mode 100644 xdp-dns/xdp_dns.bpf.c create mode 100644 xdp-dns/xdp_dns.c create mode 100644 xdp-dns/xdp_dns.h diff --git a/Makefile b/Makefile index 328fafed..93177dd2 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ include config.mk UTILS := xdp-filter xdp-loader xdp-dump ifneq ($(BPFTOOL),) -UTILS += xdp-bench xdp-monitor xdp-trafficgen xdp-synproxy xdp-dnsrrl xdp-udp +UTILS += xdp-bench xdp-monitor xdp-trafficgen xdp-synproxy xdp-dnsrrl xdp-udp xdp-dns endif SUBDIRS := lib $(UTILS) diff --git a/xdp-dns/Makefile b/xdp-dns/Makefile new file mode 100644 index 00000000..d5f3042d --- /dev/null +++ b/xdp-dns/Makefile @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) + +XDP_TARGETS := xdp_dns.bpf +BPF_SKEL_TARGETS := $(XDP_TARGETS) +USER_TARGETS := xdp_dns + +LIB_DIR = ../lib + +include $(LIB_DIR)/common.mk diff --git a/xdp-dns/xdp_dns.bpf.c b/xdp-dns/xdp_dns.bpf.c new file mode 100644 index 00000000..8339aeaa --- /dev/null +++ b/xdp-dns/xdp_dns.bpf.c @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2021, NLnet Labs. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "vmlinux_local.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bpf/compiler.h" +#include "xdp_dns.h" + +/* with vmlinux.h, define here to avoid the undefined error */ +#define ETH_P_8021Q 0x8100 /* 802.1Q VLAN Extended Header */ +#define ETH_P_8021AD 0x88A8 /* 802.1ad Service VLAN */ + +// do not use libc includes because this causes clang +// to include 32bit headers on 64bit ( only ) systems. +#define memcpy __builtin_memcpy +#define MAX_DOMAIN_SIZE 18 + +struct meta_data { + __u16 eth_proto; + __u16 ip_pos; + __u16 opt_pos; + __u16 unused; +}; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, char[MAX_DOMAIN_SIZE + 1]); + __type(value, __u8); + __uint(max_entries, 1024); + __uint(pinning, LIBBPF_PIN_BY_NAME); +} domain_denylist SEC(".maps"); + +/* + * Store the VLAN header + */ +struct vlanhdr { + __u16 tci; + __u16 encap_proto; +}; + +/* + * Helper pointer to parse the incoming packets + */ +struct cursor { + void *pos; + void *end; +}; + +static __always_inline +void cursor_init(struct cursor *c, struct xdp_md *ctx) +{ + c->end = (void *)(long)ctx->data_end; + c->pos = (void *)(long)ctx->data; +} + +#define PARSE_FUNC_DECLARATION(STRUCT) \ +static __always_inline \ +struct STRUCT *parse_ ## STRUCT (struct cursor *c) \ +{ \ + struct STRUCT *ret = c->pos; \ + if (c->pos + sizeof(struct STRUCT) > c->end) \ + return 0; \ + c->pos += sizeof(struct STRUCT); \ + return ret; \ +} + +PARSE_FUNC_DECLARATION(ethhdr) +PARSE_FUNC_DECLARATION(vlanhdr) +PARSE_FUNC_DECLARATION(iphdr) +PARSE_FUNC_DECLARATION(udphdr) +PARSE_FUNC_DECLARATION(dnshdr) + +static __always_inline +struct ethhdr *parse_eth(struct cursor *c, __u16 *eth_proto) +{ + struct ethhdr *eth; + + if (!(eth = parse_ethhdr(c))) + return 0; + + *eth_proto = eth->h_proto; + if (*eth_proto == __bpf_htons(ETH_P_8021Q) + || *eth_proto == __bpf_htons(ETH_P_8021AD)) { + struct vlanhdr *vlan; + + if (!(vlan = parse_vlanhdr(c))) + return 0; + + *eth_proto = vlan->encap_proto; + if (*eth_proto == __bpf_htons(ETH_P_8021Q) + || *eth_proto == __bpf_htons(ETH_P_8021AD)) { + if (!(vlan = parse_vlanhdr(c))) + return 0; + + *eth_proto = vlan->encap_proto; + } + } + return eth; +} + +static __always_inline char *parse_dname(struct cursor *c) { + __u8 *dname = c->pos; + __u8 i; + + for (i = 0; i < 128; i++) { /* Maximum 128 labels */ + __u8 o; + + // Check bounds before accessing the next byte + if (c->pos + 1 > c->end) + return 0; + + o = *(__u8 *)c->pos; + + // Check for DNS name compression + if ((o & 0xC0) == 0xC0) { + // If the current label is compressed, skip the next 2 bytes + if (c->pos + 2 > c->end) // Ensure we have 2 bytes to skip + return 0; + + c->pos += 2; + return (char *)dname; // Return the parsed domain name + } else if (o > 63 || c->pos + o + 1 > c->end) { + // Label is invalid or out of bounds + return 0; + } + + // Move the cursor by label length and its leading length byte + c->pos += o + 1; + + // End of domain name (null label length) + if (o == 0) + return (char *)dname; + } + + // If we exit the loop without finding a terminating label, return NULL + return 0; +} + +static __always_inline void *custom_memcpy(void *dest, const void *src, __u8 len) { + __u8 i; + + // Perform the copy byte-by-byte to satisfy the BPF verifier + for (i = 0; i < len; i++) { + *((__u8 *)dest + i) = *((__u8 *)src + i); + } + + return dest; +} + +// Custom strlen function for BPF +static __always_inline __u8 custom_strlen(const char *str, struct cursor *c) { + __u8 len = 0; + + // Loop through the string, ensuring not to exceed MAX_STRING_LEN + #pragma unroll + for (int i = 0; i < MAX_DOMAIN_SIZE; i++) { + if (str + i >= c->end) // Check if we are at or beyond the end of the packet + break; + if (str[i] == '\0') + break; + len++; + } + + return len; +} + +SEC("xdp") +int xdp_dns(struct xdp_md *ctx) +{ + struct meta_data *md = (void *)(long)ctx->data_meta; + struct cursor c; + struct ethhdr *eth; + struct iphdr *ipv4; + struct udphdr *udp; + struct dnshdr *dns; + char *qname; +// __u8 value = 1; + __u8 len = 0; + char domain_key[MAX_DOMAIN_SIZE + 1 ] = {0}; // Buffer for map lookup + + if (bpf_xdp_adjust_meta(ctx, -(int)sizeof(struct meta_data))) + return XDP_PASS; + + cursor_init(&c, ctx); + md = (void *)(long)ctx->data_meta; + if ((void *)(md + 1) > c.pos) + return XDP_PASS; + + if (!(eth = parse_eth(&c, &md->eth_proto))) + return XDP_PASS; + md->ip_pos = c.pos - (void *)eth; + + if (md->eth_proto == __bpf_htons(ETH_P_IP)) { + if (!(ipv4 = parse_iphdr(&c))) + return XDP_PASS; /* Not IPv4 */ + switch (ipv4->protocol) { + case IPPROTO_UDP: + if (!(udp = parse_udphdr(&c)) + || !(udp->dest == __bpf_htons(DNS_PORT)) + || !(dns = parse_dnshdr(&c))) + return XDP_PASS; /* Not DNS */ + + if (dns->flags.as_bits_and_pieces.qr + || dns->qdcount != __bpf_htons(1) + || dns->ancount || dns->nscount + || dns->arcount > __bpf_htons(2)) + return XDP_ABORTED; // Return FORMERR? + + qname = parse_dname(&c); + if (!qname) { + return XDP_ABORTED; // Return FORMERR? + } + + len = custom_strlen(qname, &c); + bpf_printk("qname %s len is %d from %pI4", qname, len, &ipv4->saddr); + + //avoid R2 offset is outside of the packet error + if (qname + len > c.end) + return XDP_ABORTED; // Return FORMERR? + + int copy_len = len < MAX_DOMAIN_SIZE ? len : MAX_DOMAIN_SIZE; + custom_memcpy(domain_key, qname, copy_len); + domain_key[MAX_DOMAIN_SIZE] = '\0'; // Ensure null-termination + +/* + bpf_printk("domain_key %s copy_len is %d from %pI4", domain_key, copy_len, &ipv4->saddr); + + if (bpf_map_update_elem(&domain_denylist, &domain_key, &value, BPF_ANY) < 0) { + bpf_printk("Domain %s not updated in denylist\n", domain_key); + } else { + bpf_printk("Domain %s updated in denylist\n", domain_key); + } + +*/ + if (bpf_map_lookup_elem(&domain_denylist, domain_key)) { + bpf_printk("Domain %s found in denylist, dropping packet\n", domain_key); + return XDP_DROP; + } + else { + bpf_printk("Domain %s not found in denylist\n", domain_key); + } + + break; + } + + } + return XDP_PASS; +} + +char _license[] SEC("license") = "GPL"; diff --git a/xdp-dns/xdp_dns.c b/xdp-dns/xdp_dns.c new file mode 100644 index 00000000..666aea6a --- /dev/null +++ b/xdp-dns/xdp_dns.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include + +#define MAX_DOMAIN_SIZE 128 // Increased size to handle larger domains + +// Function to encode a domain name with label lengths +static void encode_domain(const char *domain, char *encoded) { + const char *ptr = domain; + char *enc_ptr = encoded; + size_t label_len; + + while (*ptr) { + // Find the length of the current label + label_len = strcspn(ptr, "."); + if (label_len > 0) { + // Set the length of the label + *enc_ptr++ = (char)label_len; + // Copy the label itself + memcpy(enc_ptr, ptr, label_len); + enc_ptr += label_len; + } + // Move to the next label + ptr += label_len; + if (*ptr == '.') { + ptr++; // Skip the dot + } + } + // Append a zero-length label to mark the end of the domain name + *enc_ptr++ = 0; +} + +int main(int argc, char *argv[]) { + int map_fd; + char domain_key[MAX_DOMAIN_SIZE + 1] = {0}; + __u8 value = 1; + + // Check for proper number of arguments + if (argc != 2) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + + // Encode the domain name with label lengths + encode_domain(argv[1], domain_key); + + // Open the BPF map + map_fd = bpf_obj_get("/sys/fs/bpf/xdp-dns/domain_denylist"); + if (map_fd < 0) { + fprintf(stderr, "Failed to open map: %s\n", strerror(errno)); + return 1; + } + + // Update the map with the encoded domain name + if (bpf_map_update_elem(map_fd, domain_key, &value, BPF_ANY) != 0) { + fprintf(stderr, "Failed to update map: %s\n", strerror(errno)); + return 1; + } + + printf("Domain %s added to denylist\n", argv[1]); + return 0; +} + diff --git a/xdp-dns/xdp_dns.h b/xdp-dns/xdp_dns.h new file mode 100644 index 00000000..006fbe6b --- /dev/null +++ b/xdp-dns/xdp_dns.h @@ -0,0 +1,91 @@ +#define DNS_PORT 53 +#define RR_TYPE_OPT 41 + +#define FRAME_SIZE 1000000000 + +/* + * Store the DNS header + */ +struct dnshdr { + __u16 id; + union { + struct { + __u8 rd : 1; + __u8 tc : 1; + __u8 aa : 1; + __u8 opcode : 4; + __u8 qr : 1; + + __u8 rcode : 4; + __u8 cd : 1; + __u8 ad : 1; + __u8 z : 1; + __u8 ra : 1; + } as_bits_and_pieces; + __u16 as_value; + } flags; + __u16 qdcount; + __u16 ancount; + __u16 nscount; + __u16 arcount; +}; + +struct dns_qrr { + __u16 qtype; + __u16 qclass; +}; + +struct dns_rr { + __u16 type; + __u16 class; + __u32 ttl; + __u16 rdata_len; +} __attribute__((packed)); + +struct option { + __u16 code; + __u16 len; + __u8 data[]; +} __attribute__((packed)); + +/* + * Recalculate the checksum + */ +static __always_inline +void update_checksum(__u16 *csum, __u16 old_val, __u16 new_val) +{ + __u32 new_csum_value; + __u32 new_csum_comp; + __u32 undo; + + undo = ~((__u32)*csum) + ~((__u32)old_val); + new_csum_value = undo + (undo < ~((__u32)old_val)) + (__u32)new_val; + new_csum_comp = new_csum_value + (new_csum_value < ((__u32)new_val)); + new_csum_comp = (new_csum_comp & 0xFFFF) + (new_csum_comp >> 16); + new_csum_comp = (new_csum_comp & 0xFFFF) + (new_csum_comp >> 16); + *csum = (__u16)~new_csum_comp; +} + + +//TCP + +#define NSEC_PER_SEC 1000000000L + +#define ETH_ALEN 6 +#define ETH_P_IP 0x0800 +#define ETH_P_IPV6 0x86DD + +#define IP_DF 0x4000 +#define IP_MF 0x2000 +#define IP_OFFSET 0x1fff + +#define swap(a, b) \ + do { typeof(a) __tmp = (a); (a) = (b); (b) = __tmp; } while (0) + +#define __get_unaligned_t(type, ptr) ({ \ + const struct { type x; } __attribute__((__packed__)) *__pptr = (typeof(__pptr))(ptr); \ + __pptr->x; \ +}) + +#define get_unaligned(ptr) __get_unaligned_t(typeof(*(ptr)), (ptr)) +