From cb9fc3025a51dfbaf28a28457455e8dc0b4a4e48 Mon Sep 17 00:00:00 2001 From: Sachin Tiptur Date: Fri, 11 Jun 2021 01:00:27 +0200 Subject: [PATCH] Add DHCP relay program. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial working version of DHCP relay using XDP is created. Currently, this code has user program and a xdp ebpf program. User program takes network interface and dhcp relay server IP as inputs and store it in a map. XDP program filters the incoming DHCP requests and inserts option 82 in the DHCP request packets and overwrites the destination IP to that of DHCP relay server IP.An optional argu -ment for user program is also provided to unload the xdp program. README file provides to instructions to build and load the xdp program. Signed-off-by: Sachin Tiptur [ whitespace fixes ] Signed-off-by: Toke Høiland-Jørgensen --- dhcp-relay/.gitignore | 3 + dhcp-relay/Makefile | 10 ++ dhcp-relay/README | 25 +++++ dhcp-relay/dhcp-relay.h | 52 ++++++++++ dhcp-relay/dhcp_kern_xdp.c | 183 ++++++++++++++++++++++++++++++++++ dhcp-relay/dhcp_user_xdp.c | 198 +++++++++++++++++++++++++++++++++++++ 6 files changed, 471 insertions(+) create mode 100644 dhcp-relay/.gitignore create mode 100644 dhcp-relay/Makefile create mode 100644 dhcp-relay/README create mode 100644 dhcp-relay/dhcp-relay.h create mode 100644 dhcp-relay/dhcp_kern_xdp.c create mode 100644 dhcp-relay/dhcp_user_xdp.c diff --git a/dhcp-relay/.gitignore b/dhcp-relay/.gitignore new file mode 100644 index 0000000..dda8e0f --- /dev/null +++ b/dhcp-relay/.gitignore @@ -0,0 +1,3 @@ +*.ll +*.o +dhcp_user_xdp diff --git a/dhcp-relay/Makefile b/dhcp-relay/Makefile new file mode 100644 index 0000000..964f707 --- /dev/null +++ b/dhcp-relay/Makefile @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause) + +USER_TARGETS := dhcp_user_xdp +BPF_TARGETS :=dhcp_kern_xdp +EXTRA_DEPS := dhcp-relay.h +#EXTRA_CFLAGS := $(if $(IPV6),-DIPV6) + +LIB_DIR = ../lib + +include $(LIB_DIR)/common.mk diff --git a/dhcp-relay/README b/dhcp-relay/README new file mode 100644 index 0000000..7642d6d --- /dev/null +++ b/dhcp-relay/README @@ -0,0 +1,25 @@ +Usage +----- +dhcp_user_xdp takes network interface and dhcp relay server IP +as inputs and stores it in a map. Filters the incoming DHCP requests and inserts +option 82 in the DHCP request packets and overwrites the destination IP to that +of DHCP relay server IP. + +Build instructions: +cd bpf-examples/dhcp-relay +make + +Loading bpf program: +sudo ./dhcp_user_xdp -i -d +where, +netif: Ingress network interface name + +unloading program: +sudo ./dhcp_user_xdp -i -u + +To run in SKB mode: +add option "-m skb" for both load and uload commands + +Verify using tcpdump: +sudo tcpdump -s 0 -i port 67 and port 68 -vvv + diff --git a/dhcp-relay/dhcp-relay.h b/dhcp-relay/dhcp-relay.h new file mode 100644 index 0000000..ee276b9 --- /dev/null +++ b/dhcp-relay/dhcp-relay.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#include +#include + +#define XDP_PROG_SEC "xdp" + +#define DHCP_SNAME_LEN 64 +#define DHCP_FILE_LEN 128 + +#define DHO_DHCP_AGENT_OPTIONS 82 +#define RAI_CIRCUIT_ID 1 +#define RAI_REMOTE_ID 2 +#define RAI_OPTION_LEN 2 + +#define DEST_PORT 67 /* UDP destination port for dhcp */ +#define MAX_BYTES 280 /* Max bytes supported by xdp load/store apis */ + +/* structure for sub-options in option 82*/ +struct sub_option { + __u8 option_id; + __u8 len; + __u16 val; +}; + +/*structure for dhcp option 82 */ +struct dhcp_option_82 { + __u8 t; + __u8 len; + struct sub_option circuit_id; + struct sub_option remote_id; +}; + +struct dhcp_packet { + __u8 op; /* 0: Message opcode/type */ + __u8 htype; /* 1: Hardware addr type (net/if_types.h) */ + __u8 hlen; /* 2: Hardware addr length */ + __u8 hops; /* 3: Number of relay agent hops from client */ + __u32 xid; /* 4: Transaction ID */ + __u16 secs; /* 8: Seconds since client started looking */ + __u16 flags; /* 10: Flag bits */ + struct in_addr ciaddr; /* 12: Client IP address (if already in use) */ + struct in_addr yiaddr; /* 16: Client IP address */ + struct in_addr siaddr; /* 18: IP address of next server to talk to */ + struct in_addr giaddr; /* 20: DHCP relay agent IP address */ + unsigned char chaddr[16]; /* 24: Client hardware address */ + char sname[DHCP_SNAME_LEN]; /* 40: Server name */ + char file[DHCP_FILE_LEN]; /* 104: Boot filename */ + __u32 cookie; /* 232: Magic cookie */ + unsigned char options[0]; + /* 236: Optional parameters + (actual length dependent on MTU). */ +}; diff --git a/dhcp-relay/dhcp_kern_xdp.c b/dhcp-relay/dhcp_kern_xdp.c new file mode 100644 index 0000000..a55e76f --- /dev/null +++ b/dhcp-relay/dhcp_kern_xdp.c @@ -0,0 +1,183 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include +#include +#include +#include +#include +#include "dhcp-relay.h" + +/* + * This map is for storing the DHCP relay server + * IP address configured by user. It is received + * as an argument by user program. +*/ +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __type(key, __u32); + __type(value, __u32); + __uint(max_entries, 1); +} dhcp_server SEC(".maps"); + +/* Inserts DHCP option 82 into the received dhcp packet + * at the specified offset. +*/ +static __always_inline int write_dhcp_option(void *ctx, int offset, + struct collect_vlans *vlans) +{ + struct dhcp_option_82 option; + + option.t = DHO_DHCP_AGENT_OPTIONS; + option.len = 8; + option.circuit_id.option_id = RAI_CIRCUIT_ID; + option.circuit_id.len = RAI_OPTION_LEN; + option.circuit_id.val = bpf_htons(vlans->id[0]); + option.remote_id.option_id = RAI_REMOTE_ID; + option.remote_id.len = RAI_OPTION_LEN; + option.remote_id.val = bpf_htons(vlans->id[1]); + + return xdp_store_bytes(ctx, offset, &option, sizeof(option), 0); +} + +/* Calculates the IP checksum */ +static __always_inline int calc_ip_csum(struct iphdr *oldip, struct iphdr *ip, + __u32 oldcsum) +{ + __u32 size = sizeof(struct iphdr); + __u32 csum = bpf_csum_diff((__be32 *)oldip, size, (__be32 *)ip, size, + ~oldcsum); + __u32 sum = (csum >> 16) + (csum & 0xffff); + sum += (sum >> 16); + return sum; +} + +/* Offset to DHCP Options part of the packet */ +#define static_offset \ + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr) + \ + offsetof(struct dhcp_packet, options) + +/* Delta value to be adjusted at xdp head*/ +#define delta sizeof(struct dhcp_option_82) + +/* buf needs to be a static global var because the verifier won't allow + * unaligned stack accesses +*/ +static __u8 buf[static_offset + VLAN_MAX_DEPTH * sizeof(struct vlan_hdr)]; + +/* XDP program for parsing the DHCP packet and inserting the option 82*/ +SEC(XDP_PROG_SEC) +int xdp_dhcp_relay(struct xdp_md *ctx) +{ + void *data_end = (void *)(long)ctx->data_end; + void *data = (void *)(long)ctx->data; + struct collect_vlans vlans = { 0 }; + struct ethhdr *eth; + struct iphdr *ip; + struct iphdr oldip; + struct udphdr *udp; + __u32 *dhcp_srv; + int rc = XDP_PASS; + __u16 offset = static_offset; + __u16 ip_offset = 0; + int i = 0; + + /* These keep track of the next header type and iterator pointer */ + struct hdr_cursor nh; + int ether_type; + int h_proto = 0; + int key = 0; + int len = 0; + + if (data + 1 > data_end) + return XDP_ABORTED; + + nh.pos = data; + ether_type = parse_ethhdr_vlan(&nh, data_end, ð, &vlans); + /* check for valid ether type */ + if (ether_type < 0) { + rc = XDP_ABORTED; + goto out; + } + if (ether_type != bpf_htons(ETH_P_IP)) + goto out; + + /* Check at least two vlan tags are present */ + if (vlans.id[1] == 0) + goto out; + + /* Read dhcp relay server IP from map */ + dhcp_srv = bpf_map_lookup_elem(&dhcp_server, &key); + if (dhcp_srv == NULL) + goto out; + + h_proto = parse_iphdr(&nh, data_end, &ip); + + /* only handle fixed-size IP header due to static copy */ + if (h_proto != IPPROTO_UDP || ip->ihl > 5) { + goto out; + } + /*old ip hdr backup for re-calculating the checksum later*/ + oldip = *ip; + ip_offset = ((void *)ip - data) & 0x3fff; + len = parse_udphdr(&nh, data_end, &udp); + if (len < 0) + goto out; + + if (udp->dest != bpf_htons(DEST_PORT)) + goto out; + + if (xdp_load_bytes(ctx, 0, buf, static_offset)) + goto out; + + for (i = 0; i < VLAN_MAX_DEPTH; i++) { + if (vlans.id[i]) { + if (xdp_load_bytes(ctx, offset, buf + offset, 4)) + goto out; + offset += 4; + } + } + + /* adjusting the packet head by delta size to insert option82 */ + if (bpf_xdp_adjust_head(ctx, 0 - delta) < 0) + return XDP_ABORTED; + + data_end = (void *)(long)ctx->data_end; + data = (void *)(long)ctx->data; + + if (data + offset > data_end) + return XDP_ABORTED; + + if (xdp_store_bytes(ctx, 0, buf, static_offset, 0)) + return XDP_ABORTED; + + if (offset > static_offset) { + offset = static_offset; + for (i = 0; i < VLAN_MAX_DEPTH; i++) { + if (vlans.id[i]) { + if (xdp_store_bytes(ctx, offset, buf + offset, + 4, 0)) + return XDP_ABORTED; + offset += 4; + } + } + } + + if (write_dhcp_option(ctx, offset, &vlans)) + return XDP_ABORTED; + + ip = data + ip_offset; + if (ip + 1 > data_end) + return XDP_ABORTED; + + /* overwrite the destination IP in IP header */ + ip->daddr = *dhcp_srv; + + //re-calc ip checksum + __u32 sum = calc_ip_csum(&oldip, ip, oldip.check); + ip->check = ~sum; + rc = XDP_PASS; + goto out; + +out: + return rc; +} diff --git a/dhcp-relay/dhcp_user_xdp.c b/dhcp-relay/dhcp_user_xdp.c new file mode 100644 index 0000000..23f983e --- /dev/null +++ b/dhcp-relay/dhcp_user_xdp.c @@ -0,0 +1,198 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +static const char *__doc__ = "DHCP relay program to add Option 82\n"; + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include /* depend on kernel-headers installed */ +#include + +#define SERVER_MAP "dhcp_server" +#define XDP_OBJ "dhcp_kern_xdp.o" + +static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "interface", required_argument, NULL, + 'i' }, // Name of interface to run on + { "dhcp-server", required_argument, NULL, 'd' }, + { "mode", required_argument, NULL, 'm' }, + { "unload", no_argument, NULL, 'u' }, + { 0, 0, NULL, 0 } +}; + +static void print_usage(char *argv[]) +{ + int i; + printf("Usage:\n"); + printf("%s\n", argv[0]); + for (i = 0; options[i].name != 0; i++) { + printf(" --%-12s", options[i].name); + if (options[i].flag != NULL) + printf(" flag (internal value:%d)", *options[i].flag); + else + printf(" short-option: -%c", options[i].val); + printf("\n"); + } + printf("Example:\n"); + printf("To load program:\n %s -i eth0 -d 10.0.0.1\n", argv[0]); + printf("To unload program:\n %s -i eth0 -u\n", argv[0]); + printf("\n"); +} + +static int xdp_link_detach(int ifindex, __u32 xdp_flags) +{ + int err; + + if ((err = bpf_set_link_xdp_fd(ifindex, -1, xdp_flags)) < 0) { + fprintf(stderr, "ERR: link set xdp unload failed (err=%d):%s\n", + err, strerror(-err)); + return -1; + } + return 0; +} + +int xdp_link_attach(int ifindex, __u32 xdp_flags, int prog_fd) +{ + int err; + + /* libbpf provide the XDP net_device link-level hook attach helper */ + err = bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags); + + if (err < 0) { + fprintf(stderr, + "ERR: " + "ifindex(%d) link set xdp fd failed (%d): %s\n", + ifindex, -err, strerror(-err)); + + switch (-err) { + case EBUSY: + case EEXIST: + fprintf(stderr, "Hint: XDP already loaded on device\n"); + break; + case EOPNOTSUPP: + fprintf(stderr, "Hint: Native-XDP not supported\n"); + break; + default: + break; + } + return -1; + } + + return 0; +} + +/* User program takes two or three arguments + * interface name, relay server IP and prog + * unload flag +*/ +int main(int argc, char **argv) +{ + char filename[256] = "dhcp_kern_xdp.o"; + int prog_fd, err; + int opt; + + __u32 xdp_flags = XDP_FLAGS_DRV_MODE; + char dev[IF_NAMESIZE] = ""; + bool do_unload = 0; + struct bpf_map *map = NULL; + struct bpf_obj *obj = NULL; + int map_fd; + int key = 0; + char server[15] = ""; + struct in_addr addr; + __u16 ifindex; + + while ((opt = getopt_long(argc, argv, "hui:d:m:", options, NULL)) != + -1) { + switch (opt) { + case 'i': + strncpy(dev, optarg, IF_NAMESIZE); + dev[IF_NAMESIZE - 1] = '\0'; + ifindex = if_nametoindex(dev); + if (ifindex <= 0) { + printf("Couldn't find ifname:%s \n", dev); + return -EINVAL; + } + break; + case 'd': + if (inet_aton(optarg, &addr) == 0) { + fprintf(stderr, + "Couldn't validate IP address:%s\n", + optarg); + return -EINVAL; + } + break; + case 'm': + if (strcmp(optarg, "skb") == 0) { + xdp_flags = XDP_FLAGS_SKB_MODE; + } else if (strcmp(optarg, "drv") != 0) { + fprintf(stderr, "Invalid mode: %s\n", optarg); + return -EINVAL; + } + + break; + case 'u': + do_unload = 1; + break; + case 'h': + print_usage(argv); + exit(0); + default: + fprintf(stderr, "Unknown option %s\n", argv[optind]); + return -EINVAL; + } + } + + if (do_unload) + return xdp_link_detach(ifindex, xdp_flags); + + /* Load the BPF-ELF object file and get back first BPF_prog FD */ + err = bpf_prog_load(filename, BPF_PROG_TYPE_XDP, &obj, &prog_fd); + if (err) { + fprintf(stderr, "ERR: loading BPF-OBJ file(%s) (%d): %s\n", + filename, err, strerror(-err)); + return -1; + } + if (prog_fd <= 0) { + printf("ERR: loading file: %s\n"); + return -1; + } + + /* read the map from prog object file and update the real + * server IP to the map + */ + map = bpf_object__find_map_by_name(obj, SERVER_MAP); + err = libbpf_get_error(map); + if (err) { + fprintf(stderr, "Could not find map %s in %s: %s\n", SERVER_MAP, + XDP_OBJ, strerror(err)); + map = NULL; + exit(-1); + } + map_fd = bpf_map__fd(map); + if (map_fd < 0) { + fprintf(stderr, "Could not get map fd\n"); + exit(-1); + } + + err = bpf_map_update_elem(map_fd, &key, &addr.s_addr, BPF_ANY); + if (err) { + fprintf(stderr, "Could not update map %s in %s\n", SERVER_MAP, + XDP_OBJ); + exit(-1); + } + + err = xdp_link_attach(ifindex, xdp_flags, prog_fd); + if (err) + return err; + + printf("Success: Loading xdp program\n"); + return 0; +}