| /* SPDX-License-Identifier: GPL-2.0 |
| * Copyright(c) 2018 Jesper Dangaard Brouer. |
| * |
| * XDP/TC VLAN manipulation example |
| * |
| * GOTCHA: Remember to disable NIC hardware offloading of VLANs, |
| * else the VLAN tags are NOT inlined in the packet payload: |
| * |
| * # ethtool -K ixgbe2 rxvlan off |
| * |
| * Verify setting: |
| * # ethtool -k ixgbe2 | grep rx-vlan-offload |
| * rx-vlan-offload: off |
| * |
| */ |
| #include <stddef.h> |
| #include <stdbool.h> |
| #include <string.h> |
| #include <linux/bpf.h> |
| #include <linux/if_ether.h> |
| #include <linux/if_vlan.h> |
| #include <linux/in.h> |
| #include <linux/pkt_cls.h> |
| |
| #include <bpf/bpf_helpers.h> |
| #include <bpf/bpf_endian.h> |
| |
| /* linux/if_vlan.h have not exposed this as UAPI, thus mirror some here |
| * |
| * struct vlan_hdr - vlan header |
| * @h_vlan_TCI: priority and VLAN ID |
| * @h_vlan_encapsulated_proto: packet type ID or len |
| */ |
| struct _vlan_hdr { |
| __be16 h_vlan_TCI; |
| __be16 h_vlan_encapsulated_proto; |
| }; |
| #define VLAN_PRIO_MASK 0xe000 /* Priority Code Point */ |
| #define VLAN_PRIO_SHIFT 13 |
| #define VLAN_CFI_MASK 0x1000 /* Canonical Format Indicator */ |
| #define VLAN_TAG_PRESENT VLAN_CFI_MASK |
| #define VLAN_VID_MASK 0x0fff /* VLAN Identifier */ |
| #define VLAN_N_VID 4096 |
| |
| struct parse_pkt { |
| __u16 l3_proto; |
| __u16 l3_offset; |
| __u16 vlan_outer; |
| __u16 vlan_inner; |
| __u8 vlan_outer_offset; |
| __u8 vlan_inner_offset; |
| }; |
| |
| char _license[] SEC("license") = "GPL"; |
| |
| static __always_inline |
| bool parse_eth_frame(struct ethhdr *eth, void *data_end, struct parse_pkt *pkt) |
| { |
| __u16 eth_type; |
| __u8 offset; |
| |
| offset = sizeof(*eth); |
| /* Make sure packet is large enough for parsing eth + 2 VLAN headers */ |
| if ((void *)eth + offset + (2*sizeof(struct _vlan_hdr)) > data_end) |
| return false; |
| |
| eth_type = eth->h_proto; |
| |
| /* Handle outer VLAN tag */ |
| if (eth_type == bpf_htons(ETH_P_8021Q) |
| || eth_type == bpf_htons(ETH_P_8021AD)) { |
| struct _vlan_hdr *vlan_hdr; |
| |
| vlan_hdr = (void *)eth + offset; |
| pkt->vlan_outer_offset = offset; |
| pkt->vlan_outer = bpf_ntohs(vlan_hdr->h_vlan_TCI) |
| & VLAN_VID_MASK; |
| eth_type = vlan_hdr->h_vlan_encapsulated_proto; |
| offset += sizeof(*vlan_hdr); |
| } |
| |
| /* Handle inner (double) VLAN tag */ |
| if (eth_type == bpf_htons(ETH_P_8021Q) |
| || eth_type == bpf_htons(ETH_P_8021AD)) { |
| struct _vlan_hdr *vlan_hdr; |
| |
| vlan_hdr = (void *)eth + offset; |
| pkt->vlan_inner_offset = offset; |
| pkt->vlan_inner = bpf_ntohs(vlan_hdr->h_vlan_TCI) |
| & VLAN_VID_MASK; |
| eth_type = vlan_hdr->h_vlan_encapsulated_proto; |
| offset += sizeof(*vlan_hdr); |
| } |
| |
| pkt->l3_proto = bpf_ntohs(eth_type); /* Convert to host-byte-order */ |
| pkt->l3_offset = offset; |
| |
| return true; |
| } |
| |
| /* Hint, VLANs are chosen to hit network-byte-order issues */ |
| #define TESTVLAN 4011 /* 0xFAB */ |
| // #define TO_VLAN 4000 /* 0xFA0 (hint 0xOA0 = 160) */ |
| |
| SEC("xdp_drop_vlan_4011") |
| int xdp_prognum0(struct xdp_md *ctx) |
| { |
| void *data_end = (void *)(long)ctx->data_end; |
| void *data = (void *)(long)ctx->data; |
| struct parse_pkt pkt = { 0 }; |
| |
| if (!parse_eth_frame(data, data_end, &pkt)) |
| return XDP_ABORTED; |
| |
| /* Drop specific VLAN ID example */ |
| if (pkt.vlan_outer == TESTVLAN) |
| return XDP_ABORTED; |
| /* |
| * Using XDP_ABORTED makes it possible to record this event, |
| * via tracepoint xdp:xdp_exception like: |
| * # perf record -a -e xdp:xdp_exception |
| * # perf script |
| */ |
| return XDP_PASS; |
| } |
| /* |
| Commands to setup VLAN on Linux to test packets gets dropped: |
| |
| export ROOTDEV=ixgbe2 |
| export VLANID=4011 |
| ip link add link $ROOTDEV name $ROOTDEV.$VLANID type vlan id $VLANID |
| ip link set dev $ROOTDEV.$VLANID up |
| |
| ip link set dev $ROOTDEV mtu 1508 |
| ip addr add 100.64.40.11/24 dev $ROOTDEV.$VLANID |
| |
| Load prog with ip tool: |
| |
| ip link set $ROOTDEV xdp off |
| ip link set $ROOTDEV xdp object xdp_vlan01_kern.o section xdp_drop_vlan_4011 |
| |
| */ |
| |
| /* Changing VLAN to zero, have same practical effect as removing the VLAN. */ |
| #define TO_VLAN 0 |
| |
| SEC("xdp_vlan_change") |
| int xdp_prognum1(struct xdp_md *ctx) |
| { |
| void *data_end = (void *)(long)ctx->data_end; |
| void *data = (void *)(long)ctx->data; |
| struct parse_pkt pkt = { 0 }; |
| |
| if (!parse_eth_frame(data, data_end, &pkt)) |
| return XDP_ABORTED; |
| |
| /* Change specific VLAN ID */ |
| if (pkt.vlan_outer == TESTVLAN) { |
| struct _vlan_hdr *vlan_hdr = data + pkt.vlan_outer_offset; |
| |
| /* Modifying VLAN, preserve top 4 bits */ |
| vlan_hdr->h_vlan_TCI = |
| bpf_htons((bpf_ntohs(vlan_hdr->h_vlan_TCI) & 0xf000) |
| | TO_VLAN); |
| } |
| |
| return XDP_PASS; |
| } |
| |
| /* |
| * Show XDP+TC can cooperate, on creating a VLAN rewriter. |
| * 1. Create a XDP prog that can "pop"/remove a VLAN header. |
| * 2. Create a TC-bpf prog that egress can add a VLAN header. |
| */ |
| |
| #ifndef ETH_ALEN /* Ethernet MAC address length */ |
| #define ETH_ALEN 6 /* bytes */ |
| #endif |
| #define VLAN_HDR_SZ 4 /* bytes */ |
| |
| SEC("xdp_vlan_remove_outer") |
| int xdp_prognum2(struct xdp_md *ctx) |
| { |
| void *data_end = (void *)(long)ctx->data_end; |
| void *data = (void *)(long)ctx->data; |
| struct parse_pkt pkt = { 0 }; |
| char *dest; |
| |
| if (!parse_eth_frame(data, data_end, &pkt)) |
| return XDP_ABORTED; |
| |
| /* Skip packet if no outer VLAN was detected */ |
| if (pkt.vlan_outer_offset == 0) |
| return XDP_PASS; |
| |
| /* Moving Ethernet header, dest overlap with src, memmove handle this */ |
| dest = data; |
| dest += VLAN_HDR_SZ; |
| /* |
| * Notice: Taking over vlan_hdr->h_vlan_encapsulated_proto, by |
| * only moving two MAC addrs (12 bytes), not overwriting last 2 bytes |
| */ |
| __builtin_memmove(dest, data, ETH_ALEN * 2); |
| /* Note: LLVM built-in memmove inlining require size to be constant */ |
| |
| /* Move start of packet header seen by Linux kernel stack */ |
| bpf_xdp_adjust_head(ctx, VLAN_HDR_SZ); |
| |
| return XDP_PASS; |
| } |
| |
| static __always_inline |
| void shift_mac_4bytes_32bit(void *data) |
| { |
| __u32 *p = data; |
| |
| /* Assuming VLAN hdr present. The 4 bytes in p[3] that gets |
| * overwritten, is ethhdr->h_proto and vlan_hdr->h_vlan_TCI. |
| * The vlan_hdr->h_vlan_encapsulated_proto take over role as |
| * ethhdr->h_proto. |
| */ |
| p[3] = p[2]; |
| p[2] = p[1]; |
| p[1] = p[0]; |
| } |
| |
| SEC("xdp_vlan_remove_outer2") |
| int xdp_prognum3(struct xdp_md *ctx) |
| { |
| void *data_end = (void *)(long)ctx->data_end; |
| void *data = (void *)(long)ctx->data; |
| struct ethhdr *orig_eth = data; |
| struct parse_pkt pkt = { 0 }; |
| |
| if (!parse_eth_frame(orig_eth, data_end, &pkt)) |
| return XDP_ABORTED; |
| |
| /* Skip packet if no outer VLAN was detected */ |
| if (pkt.vlan_outer_offset == 0) |
| return XDP_PASS; |
| |
| /* Simply shift down MAC addrs 4 bytes, overwrite h_proto + TCI */ |
| shift_mac_4bytes_32bit(data); |
| |
| /* Move start of packet header seen by Linux kernel stack */ |
| bpf_xdp_adjust_head(ctx, VLAN_HDR_SZ); |
| |
| return XDP_PASS; |
| } |
| |
| /*===================================== |
| * BELOW: TC-hook based ebpf programs |
| * ==================================== |
| * The TC-clsact eBPF programs (currently) need to be attach via TC commands |
| */ |
| |
| SEC("tc_vlan_push") |
| int _tc_progA(struct __sk_buff *ctx) |
| { |
| bpf_skb_vlan_push(ctx, bpf_htons(ETH_P_8021Q), TESTVLAN); |
| |
| return TC_ACT_OK; |
| } |
| /* |
| Commands to setup TC to use above bpf prog: |
| |
| export ROOTDEV=ixgbe2 |
| export FILE=xdp_vlan01_kern.o |
| |
| # Re-attach clsact to clear/flush existing role |
| tc qdisc del dev $ROOTDEV clsact 2> /dev/null ;\ |
| tc qdisc add dev $ROOTDEV clsact |
| |
| # Attach BPF prog EGRESS |
| tc filter add dev $ROOTDEV egress \ |
| prio 1 handle 1 bpf da obj $FILE sec tc_vlan_push |
| |
| tc filter show dev $ROOTDEV egress |
| */ |