| // SPDX-License-Identifier: GPL-2.0 |
| /* Copyright (c) 2020 Facebook */ |
| |
| #include <stddef.h> |
| #include <errno.h> |
| #include <stdbool.h> |
| #include <sys/types.h> |
| #include <sys/socket.h> |
| #include <linux/ipv6.h> |
| #include <linux/tcp.h> |
| #include <linux/socket.h> |
| #include <linux/bpf.h> |
| #include <linux/types.h> |
| #include <bpf/bpf_helpers.h> |
| #include <bpf/bpf_endian.h> |
| #define BPF_PROG_TEST_TCP_HDR_OPTIONS |
| #include "test_tcp_hdr_options.h" |
| |
| __u16 last_addr16_n = __bpf_htons(1); |
| __u16 active_lport_n = 0; |
| __u16 active_lport_h = 0; |
| __u16 passive_lport_n = 0; |
| __u16 passive_lport_h = 0; |
| |
| /* options received at passive side */ |
| unsigned int nr_pure_ack = 0; |
| unsigned int nr_data = 0; |
| unsigned int nr_syn = 0; |
| unsigned int nr_fin = 0; |
| unsigned int nr_hwtstamp = 0; |
| |
| /* Check the header received from the active side */ |
| static int __check_active_hdr_in(struct bpf_sock_ops *skops, bool check_syn) |
| { |
| union { |
| struct tcphdr th; |
| struct ipv6hdr ip6; |
| struct tcp_exprm_opt exprm_opt; |
| struct tcp_opt reg_opt; |
| __u8 data[100]; /* IPv6 (40) + Max TCP hdr (60) */ |
| } hdr = {}; |
| __u64 load_flags = check_syn ? BPF_LOAD_HDR_OPT_TCP_SYN : 0; |
| struct tcphdr *pth; |
| int ret; |
| |
| hdr.reg_opt.kind = 0xB9; |
| |
| /* The option is 4 bytes long instead of 2 bytes */ |
| ret = bpf_load_hdr_opt(skops, &hdr.reg_opt, 2, load_flags); |
| if (ret != -ENOSPC) |
| RET_CG_ERR(ret); |
| |
| /* Test searching magic with regular kind */ |
| hdr.reg_opt.len = 4; |
| ret = bpf_load_hdr_opt(skops, &hdr.reg_opt, sizeof(hdr.reg_opt), |
| load_flags); |
| if (ret != -EINVAL) |
| RET_CG_ERR(ret); |
| |
| hdr.reg_opt.len = 0; |
| ret = bpf_load_hdr_opt(skops, &hdr.reg_opt, sizeof(hdr.reg_opt), |
| load_flags); |
| if (ret != 4 || hdr.reg_opt.len != 4 || hdr.reg_opt.kind != 0xB9 || |
| hdr.reg_opt.data[0] != 0xfa || hdr.reg_opt.data[1] != 0xce) |
| RET_CG_ERR(ret); |
| |
| /* Test searching experimental option with invalid kind length */ |
| hdr.exprm_opt.kind = TCPOPT_EXP; |
| hdr.exprm_opt.len = 5; |
| hdr.exprm_opt.magic = 0; |
| ret = bpf_load_hdr_opt(skops, &hdr.exprm_opt, sizeof(hdr.exprm_opt), |
| load_flags); |
| if (ret != -EINVAL) |
| RET_CG_ERR(ret); |
| |
| /* Test searching experimental option with 0 magic value */ |
| hdr.exprm_opt.len = 4; |
| ret = bpf_load_hdr_opt(skops, &hdr.exprm_opt, sizeof(hdr.exprm_opt), |
| load_flags); |
| if (ret != -ENOMSG) |
| RET_CG_ERR(ret); |
| |
| hdr.exprm_opt.magic = __bpf_htons(0xeB9F); |
| ret = bpf_load_hdr_opt(skops, &hdr.exprm_opt, sizeof(hdr.exprm_opt), |
| load_flags); |
| if (ret != 4 || hdr.exprm_opt.len != 4 || |
| hdr.exprm_opt.kind != TCPOPT_EXP || |
| hdr.exprm_opt.magic != __bpf_htons(0xeB9F)) |
| RET_CG_ERR(ret); |
| |
| if (!check_syn) |
| return CG_OK; |
| |
| /* Test loading from skops->syn_skb if sk_state == TCP_NEW_SYN_RECV |
| * |
| * Test loading from tp->saved_syn for other sk_state. |
| */ |
| ret = bpf_getsockopt(skops, SOL_TCP, TCP_BPF_SYN_IP, &hdr.ip6, |
| sizeof(hdr.ip6)); |
| if (ret != -ENOSPC) |
| RET_CG_ERR(ret); |
| |
| if (hdr.ip6.saddr.s6_addr16[7] != last_addr16_n || |
| hdr.ip6.daddr.s6_addr16[7] != last_addr16_n) |
| RET_CG_ERR(0); |
| |
| ret = bpf_getsockopt(skops, SOL_TCP, TCP_BPF_SYN_IP, &hdr, sizeof(hdr)); |
| if (ret < 0) |
| RET_CG_ERR(ret); |
| |
| pth = (struct tcphdr *)(&hdr.ip6 + 1); |
| if (pth->dest != passive_lport_n || pth->source != active_lport_n) |
| RET_CG_ERR(0); |
| |
| ret = bpf_getsockopt(skops, SOL_TCP, TCP_BPF_SYN, &hdr, sizeof(hdr)); |
| if (ret < 0) |
| RET_CG_ERR(ret); |
| |
| if (hdr.th.dest != passive_lport_n || hdr.th.source != active_lport_n) |
| RET_CG_ERR(0); |
| |
| return CG_OK; |
| } |
| |
| static int check_active_syn_in(struct bpf_sock_ops *skops) |
| { |
| return __check_active_hdr_in(skops, true); |
| } |
| |
| static int check_active_hdr_in(struct bpf_sock_ops *skops) |
| { |
| struct tcphdr *th; |
| |
| if (__check_active_hdr_in(skops, false) == CG_ERR) |
| return CG_ERR; |
| |
| th = skops->skb_data; |
| if (th + 1 > skops->skb_data_end) |
| RET_CG_ERR(0); |
| |
| if (tcp_hdrlen(th) < skops->skb_len) |
| nr_data++; |
| |
| if (th->fin) |
| nr_fin++; |
| |
| if (th->ack && !th->fin && tcp_hdrlen(th) == skops->skb_len) |
| nr_pure_ack++; |
| |
| if (skops->skb_hwtstamp) |
| nr_hwtstamp++; |
| |
| return CG_OK; |
| } |
| |
| static int active_opt_len(struct bpf_sock_ops *skops) |
| { |
| int err; |
| |
| /* Reserve more than enough to allow the -EEXIST test in |
| * the write_active_opt(). |
| */ |
| err = bpf_reserve_hdr_opt(skops, 12, 0); |
| if (err) |
| RET_CG_ERR(err); |
| |
| return CG_OK; |
| } |
| |
| static int write_active_opt(struct bpf_sock_ops *skops) |
| { |
| struct tcp_exprm_opt exprm_opt = {}; |
| struct tcp_opt win_scale_opt = {}; |
| struct tcp_opt reg_opt = {}; |
| struct tcphdr *th; |
| int err, ret; |
| |
| exprm_opt.kind = TCPOPT_EXP; |
| exprm_opt.len = 4; |
| exprm_opt.magic = __bpf_htons(0xeB9F); |
| |
| reg_opt.kind = 0xB9; |
| reg_opt.len = 4; |
| reg_opt.data[0] = 0xfa; |
| reg_opt.data[1] = 0xce; |
| |
| win_scale_opt.kind = TCPOPT_WINDOW; |
| |
| err = bpf_store_hdr_opt(skops, &exprm_opt, sizeof(exprm_opt), 0); |
| if (err) |
| RET_CG_ERR(err); |
| |
| /* Store the same exprm option */ |
| err = bpf_store_hdr_opt(skops, &exprm_opt, sizeof(exprm_opt), 0); |
| if (err != -EEXIST) |
| RET_CG_ERR(err); |
| |
| err = bpf_store_hdr_opt(skops, ®_opt, sizeof(reg_opt), 0); |
| if (err) |
| RET_CG_ERR(err); |
| err = bpf_store_hdr_opt(skops, ®_opt, sizeof(reg_opt), 0); |
| if (err != -EEXIST) |
| RET_CG_ERR(err); |
| |
| /* Check the option has been written and can be searched */ |
| ret = bpf_load_hdr_opt(skops, &exprm_opt, sizeof(exprm_opt), 0); |
| if (ret != 4 || exprm_opt.len != 4 || exprm_opt.kind != TCPOPT_EXP || |
| exprm_opt.magic != __bpf_htons(0xeB9F)) |
| RET_CG_ERR(ret); |
| |
| reg_opt.len = 0; |
| ret = bpf_load_hdr_opt(skops, ®_opt, sizeof(reg_opt), 0); |
| if (ret != 4 || reg_opt.len != 4 || reg_opt.kind != 0xB9 || |
| reg_opt.data[0] != 0xfa || reg_opt.data[1] != 0xce) |
| RET_CG_ERR(ret); |
| |
| th = skops->skb_data; |
| if (th + 1 > skops->skb_data_end) |
| RET_CG_ERR(0); |
| |
| if (th->syn) { |
| active_lport_h = skops->local_port; |
| active_lport_n = th->source; |
| |
| /* Search the win scale option written by kernel |
| * in the SYN packet. |
| */ |
| ret = bpf_load_hdr_opt(skops, &win_scale_opt, |
| sizeof(win_scale_opt), 0); |
| if (ret != 3 || win_scale_opt.len != 3 || |
| win_scale_opt.kind != TCPOPT_WINDOW) |
| RET_CG_ERR(ret); |
| |
| /* Write the win scale option that kernel |
| * has already written. |
| */ |
| err = bpf_store_hdr_opt(skops, &win_scale_opt, |
| sizeof(win_scale_opt), 0); |
| if (err != -EEXIST) |
| RET_CG_ERR(err); |
| } |
| |
| return CG_OK; |
| } |
| |
| static int handle_hdr_opt_len(struct bpf_sock_ops *skops) |
| { |
| __u8 tcp_flags = skops_tcp_flags(skops); |
| |
| if ((tcp_flags & TCPHDR_SYNACK) == TCPHDR_SYNACK) |
| /* Check the SYN from bpf_sock_ops_kern->syn_skb */ |
| return check_active_syn_in(skops); |
| |
| /* Passive side should have cleared the write hdr cb by now */ |
| if (skops->local_port == passive_lport_h) |
| RET_CG_ERR(0); |
| |
| return active_opt_len(skops); |
| } |
| |
| static int handle_write_hdr_opt(struct bpf_sock_ops *skops) |
| { |
| if (skops->local_port == passive_lport_h) |
| RET_CG_ERR(0); |
| |
| return write_active_opt(skops); |
| } |
| |
| static int handle_parse_hdr(struct bpf_sock_ops *skops) |
| { |
| /* Passive side is not writing any non-standard/unknown |
| * option, so the active side should never be called. |
| */ |
| if (skops->local_port == active_lport_h) |
| RET_CG_ERR(0); |
| |
| return check_active_hdr_in(skops); |
| } |
| |
| static int handle_passive_estab(struct bpf_sock_ops *skops) |
| { |
| int err; |
| |
| /* No more write hdr cb */ |
| bpf_sock_ops_cb_flags_set(skops, |
| skops->bpf_sock_ops_cb_flags & |
| ~BPF_SOCK_OPS_WRITE_HDR_OPT_CB_FLAG); |
| |
| /* Recheck the SYN but check the tp->saved_syn this time */ |
| err = check_active_syn_in(skops); |
| if (err == CG_ERR) |
| return err; |
| |
| nr_syn++; |
| |
| /* The ack has header option written by the active side also */ |
| return check_active_hdr_in(skops); |
| } |
| |
| SEC("sockops") |
| int misc_estab(struct bpf_sock_ops *skops) |
| { |
| int true_val = 1; |
| |
| switch (skops->op) { |
| case BPF_SOCK_OPS_TCP_LISTEN_CB: |
| passive_lport_h = skops->local_port; |
| passive_lport_n = __bpf_htons(passive_lport_h); |
| bpf_setsockopt(skops, SOL_TCP, TCP_SAVE_SYN, |
| &true_val, sizeof(true_val)); |
| set_hdr_cb_flags(skops, 0); |
| break; |
| case BPF_SOCK_OPS_TCP_CONNECT_CB: |
| set_hdr_cb_flags(skops, 0); |
| break; |
| case BPF_SOCK_OPS_PARSE_HDR_OPT_CB: |
| return handle_parse_hdr(skops); |
| case BPF_SOCK_OPS_HDR_OPT_LEN_CB: |
| return handle_hdr_opt_len(skops); |
| case BPF_SOCK_OPS_WRITE_HDR_OPT_CB: |
| return handle_write_hdr_opt(skops); |
| case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: |
| return handle_passive_estab(skops); |
| } |
| |
| return CG_OK; |
| } |
| |
| char _license[] SEC("license") = "GPL"; |