1 /* SPDX-License-Identifier: GPL-2.0
2 * Copyright(c) 2018 Jesper Dangaard Brouer.
3 *
4 * XDP/TC VLAN manipulation example
5 *
6 * GOTCHA: Remember to disable NIC hardware offloading of VLANs,
7 * else the VLAN tags are NOT inlined in the packet payload:
8 *
9 * # ethtool -K ixgbe2 rxvlan off
10 *
11 * Verify setting:
12 * # ethtool -k ixgbe2 | grep rx-vlan-offload
13 * rx-vlan-offload: off
14 *
15 */
16 #include <stddef.h>
17 #include <stdbool.h>
18 #include <string.h>
19 #include <linux/bpf.h>
20 #include <linux/if_ether.h>
21 #include <linux/if_vlan.h>
22 #include <linux/in.h>
23 #include <linux/pkt_cls.h>
24
25 #include <bpf/bpf_helpers.h>
26 #include <bpf/bpf_endian.h>
27
28 /* linux/if_vlan.h have not exposed this as UAPI, thus mirror some here
29 *
30 * struct vlan_hdr - vlan header
31 * @h_vlan_TCI: priority and VLAN ID
32 * @h_vlan_encapsulated_proto: packet type ID or len
33 */
34 struct _vlan_hdr {
35 __be16 h_vlan_TCI;
36 __be16 h_vlan_encapsulated_proto;
37 };
38 #define VLAN_PRIO_MASK 0xe000 /* Priority Code Point */
39 #define VLAN_PRIO_SHIFT 13
40 #define VLAN_CFI_MASK 0x1000 /* Canonical Format Indicator */
41 #define VLAN_TAG_PRESENT VLAN_CFI_MASK
42 #define VLAN_VID_MASK 0x0fff /* VLAN Identifier */
43 #define VLAN_N_VID 4096
44
45 struct parse_pkt {
46 __u16 l3_proto;
47 __u16 l3_offset;
48 __u16 vlan_outer;
49 __u16 vlan_inner;
50 __u8 vlan_outer_offset;
51 __u8 vlan_inner_offset;
52 };
53
54 char _license[] SEC("license") = "GPL";
55
56 static __always_inline
parse_eth_frame(struct ethhdr * eth,void * data_end,struct parse_pkt * pkt)57 bool parse_eth_frame(struct ethhdr *eth, void *data_end, struct parse_pkt *pkt)
58 {
59 __u16 eth_type;
60 __u8 offset;
61
62 offset = sizeof(*eth);
63 /* Make sure packet is large enough for parsing eth + 2 VLAN headers */
64 if ((void *)eth + offset + (2*sizeof(struct _vlan_hdr)) > data_end)
65 return false;
66
67 eth_type = eth->h_proto;
68
69 /* Handle outer VLAN tag */
70 if (eth_type == bpf_htons(ETH_P_8021Q)
71 || eth_type == bpf_htons(ETH_P_8021AD)) {
72 struct _vlan_hdr *vlan_hdr;
73
74 vlan_hdr = (void *)eth + offset;
75 pkt->vlan_outer_offset = offset;
76 pkt->vlan_outer = bpf_ntohs(vlan_hdr->h_vlan_TCI)
77 & VLAN_VID_MASK;
78 eth_type = vlan_hdr->h_vlan_encapsulated_proto;
79 offset += sizeof(*vlan_hdr);
80 }
81
82 /* Handle inner (double) VLAN tag */
83 if (eth_type == bpf_htons(ETH_P_8021Q)
84 || eth_type == bpf_htons(ETH_P_8021AD)) {
85 struct _vlan_hdr *vlan_hdr;
86
87 vlan_hdr = (void *)eth + offset;
88 pkt->vlan_inner_offset = offset;
89 pkt->vlan_inner = bpf_ntohs(vlan_hdr->h_vlan_TCI)
90 & VLAN_VID_MASK;
91 eth_type = vlan_hdr->h_vlan_encapsulated_proto;
92 offset += sizeof(*vlan_hdr);
93 }
94
95 pkt->l3_proto = bpf_ntohs(eth_type); /* Convert to host-byte-order */
96 pkt->l3_offset = offset;
97
98 return true;
99 }
100
101 /* Hint, VLANs are chosen to hit network-byte-order issues */
102 #define TESTVLAN 4011 /* 0xFAB */
103 // #define TO_VLAN 4000 /* 0xFA0 (hint 0xOA0 = 160) */
104
105 SEC("xdp_drop_vlan_4011")
xdp_prognum0(struct xdp_md * ctx)106 int xdp_prognum0(struct xdp_md *ctx)
107 {
108 void *data_end = (void *)(long)ctx->data_end;
109 void *data = (void *)(long)ctx->data;
110 struct parse_pkt pkt = { 0 };
111
112 if (!parse_eth_frame(data, data_end, &pkt))
113 return XDP_ABORTED;
114
115 /* Drop specific VLAN ID example */
116 if (pkt.vlan_outer == TESTVLAN)
117 return XDP_ABORTED;
118 /*
119 * Using XDP_ABORTED makes it possible to record this event,
120 * via tracepoint xdp:xdp_exception like:
121 * # perf record -a -e xdp:xdp_exception
122 * # perf script
123 */
124 return XDP_PASS;
125 }
126 /*
127 Commands to setup VLAN on Linux to test packets gets dropped:
128
129 export ROOTDEV=ixgbe2
130 export VLANID=4011
131 ip link add link $ROOTDEV name $ROOTDEV.$VLANID type vlan id $VLANID
132 ip link set dev $ROOTDEV.$VLANID up
133
134 ip link set dev $ROOTDEV mtu 1508
135 ip addr add 100.64.40.11/24 dev $ROOTDEV.$VLANID
136
137 Load prog with ip tool:
138
139 ip link set $ROOTDEV xdp off
140 ip link set $ROOTDEV xdp object xdp_vlan01_kern.o section xdp_drop_vlan_4011
141
142 */
143
144 /* Changing VLAN to zero, have same practical effect as removing the VLAN. */
145 #define TO_VLAN 0
146
147 SEC("xdp_vlan_change")
xdp_prognum1(struct xdp_md * ctx)148 int xdp_prognum1(struct xdp_md *ctx)
149 {
150 void *data_end = (void *)(long)ctx->data_end;
151 void *data = (void *)(long)ctx->data;
152 struct parse_pkt pkt = { 0 };
153
154 if (!parse_eth_frame(data, data_end, &pkt))
155 return XDP_ABORTED;
156
157 /* Change specific VLAN ID */
158 if (pkt.vlan_outer == TESTVLAN) {
159 struct _vlan_hdr *vlan_hdr = data + pkt.vlan_outer_offset;
160
161 /* Modifying VLAN, preserve top 4 bits */
162 vlan_hdr->h_vlan_TCI =
163 bpf_htons((bpf_ntohs(vlan_hdr->h_vlan_TCI) & 0xf000)
164 | TO_VLAN);
165 }
166
167 return XDP_PASS;
168 }
169
170 /*
171 * Show XDP+TC can cooperate, on creating a VLAN rewriter.
172 * 1. Create a XDP prog that can "pop"/remove a VLAN header.
173 * 2. Create a TC-bpf prog that egress can add a VLAN header.
174 */
175
176 #ifndef ETH_ALEN /* Ethernet MAC address length */
177 #define ETH_ALEN 6 /* bytes */
178 #endif
179 #define VLAN_HDR_SZ 4 /* bytes */
180
181 SEC("xdp_vlan_remove_outer")
xdp_prognum2(struct xdp_md * ctx)182 int xdp_prognum2(struct xdp_md *ctx)
183 {
184 void *data_end = (void *)(long)ctx->data_end;
185 void *data = (void *)(long)ctx->data;
186 struct parse_pkt pkt = { 0 };
187 char *dest;
188
189 if (!parse_eth_frame(data, data_end, &pkt))
190 return XDP_ABORTED;
191
192 /* Skip packet if no outer VLAN was detected */
193 if (pkt.vlan_outer_offset == 0)
194 return XDP_PASS;
195
196 /* Moving Ethernet header, dest overlap with src, memmove handle this */
197 dest = data;
198 dest += VLAN_HDR_SZ;
199 /*
200 * Notice: Taking over vlan_hdr->h_vlan_encapsulated_proto, by
201 * only moving two MAC addrs (12 bytes), not overwriting last 2 bytes
202 */
203 __builtin_memmove(dest, data, ETH_ALEN * 2);
204 /* Note: LLVM built-in memmove inlining require size to be constant */
205
206 /* Move start of packet header seen by Linux kernel stack */
207 bpf_xdp_adjust_head(ctx, VLAN_HDR_SZ);
208
209 return XDP_PASS;
210 }
211
212 static __always_inline
shift_mac_4bytes_32bit(void * data)213 void shift_mac_4bytes_32bit(void *data)
214 {
215 __u32 *p = data;
216
217 /* Assuming VLAN hdr present. The 4 bytes in p[3] that gets
218 * overwritten, is ethhdr->h_proto and vlan_hdr->h_vlan_TCI.
219 * The vlan_hdr->h_vlan_encapsulated_proto take over role as
220 * ethhdr->h_proto.
221 */
222 p[3] = p[2];
223 p[2] = p[1];
224 p[1] = p[0];
225 }
226
227 SEC("xdp_vlan_remove_outer2")
xdp_prognum3(struct xdp_md * ctx)228 int xdp_prognum3(struct xdp_md *ctx)
229 {
230 void *data_end = (void *)(long)ctx->data_end;
231 void *data = (void *)(long)ctx->data;
232 struct ethhdr *orig_eth = data;
233 struct parse_pkt pkt = { 0 };
234
235 if (!parse_eth_frame(orig_eth, data_end, &pkt))
236 return XDP_ABORTED;
237
238 /* Skip packet if no outer VLAN was detected */
239 if (pkt.vlan_outer_offset == 0)
240 return XDP_PASS;
241
242 /* Simply shift down MAC addrs 4 bytes, overwrite h_proto + TCI */
243 shift_mac_4bytes_32bit(data);
244
245 /* Move start of packet header seen by Linux kernel stack */
246 bpf_xdp_adjust_head(ctx, VLAN_HDR_SZ);
247
248 return XDP_PASS;
249 }
250
251 /*=====================================
252 * BELOW: TC-hook based ebpf programs
253 * ====================================
254 * The TC-clsact eBPF programs (currently) need to be attach via TC commands
255 */
256
257 SEC("tc_vlan_push")
_tc_progA(struct __sk_buff * ctx)258 int _tc_progA(struct __sk_buff *ctx)
259 {
260 bpf_skb_vlan_push(ctx, bpf_htons(ETH_P_8021Q), TESTVLAN);
261
262 return TC_ACT_OK;
263 }
264 /*
265 Commands to setup TC to use above bpf prog:
266
267 export ROOTDEV=ixgbe2
268 export FILE=xdp_vlan01_kern.o
269
270 # Re-attach clsact to clear/flush existing role
271 tc qdisc del dev $ROOTDEV clsact 2> /dev/null ;\
272 tc qdisc add dev $ROOTDEV clsact
273
274 # Attach BPF prog EGRESS
275 tc filter add dev $ROOTDEV egress \
276 prio 1 handle 1 bpf da obj $FILE sec tc_vlan_push
277
278 tc filter show dev $ROOTDEV egress
279 */
280