Skip to content

Commit

Permalink
Add xdp-dns for domain blocking
Browse files Browse the repository at this point in the history
use eBPF hash map for domain name lookup

Signed-off-by: Vincent Li <[email protected]>
  • Loading branch information
vincentmli committed Sep 10, 2024
1 parent 6c972a1 commit 872c9b9
Show file tree
Hide file tree
Showing 5 changed files with 444 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions xdp-dns/Makefile
Original file line number Diff line number Diff line change
@@ -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
277 changes: 277 additions & 0 deletions xdp-dns/xdp_dns.bpf.c
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

#include "vmlinux_local.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/icmp.h>
#include <linux/icmpv6.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <asm/errno.h>

#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";
66 changes: 66 additions & 0 deletions xdp-dns/xdp_dns.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

#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 <domain>\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;
}

Loading

0 comments on commit 872c9b9

Please sign in to comment.