selftests/bpf: Selftest for sys_connect hooks
authorAndrey Ignatov <rdna@fb.com>
Fri, 30 Mar 2018 22:08:06 +0000 (15:08 -0700)
committerDaniel Borkmann <daniel@iogearbox.net>
Sat, 31 Mar 2018 00:16:14 +0000 (02:16 +0200)
Add selftest for BPF_CGROUP_INET4_CONNECT and BPF_CGROUP_INET6_CONNECT
attach types.

Try to connect(2) to specified IP:port and test that:
* remote IP:port pair is overridden;
* local end of connection is bound to specified IP.

All combinations of IPv4/IPv6 and TCP/UDP are tested.

Example:
  # tcpdump -pn -i lo -w connect.pcap 2>/dev/null &
  [1] 478
  # strace -qqf -e connect -o connect.trace ./test_sock_addr.sh
  Wait for testing IPv4/IPv6 to become available ... OK
  Load bind4 with invalid type (can pollute stderr) ... REJECTED
  Load bind4 with valid type ... OK
  Attach bind4 with invalid type ... REJECTED
  Attach bind4 with valid type ... OK
  Load connect4 with invalid type (can pollute stderr) libbpf: load bpf \
    program failed: Permission denied
  libbpf: -- BEGIN DUMP LOG ---
  libbpf:
  0: (b7) r2 = 23569
  1: (63) *(u32 *)(r1 +24) = r2
  2: (b7) r2 = 16777343
  3: (63) *(u32 *)(r1 +4) = r2
  invalid bpf_context access off=4 size=4
  [ 1518.404609] random: crng init done

  libbpf: -- END LOG --
  libbpf: failed to load program 'cgroup/connect4'
  libbpf: failed to load object './connect4_prog.o'
  ... REJECTED
  Load connect4 with valid type ... OK
  Attach connect4 with invalid type ... REJECTED
  Attach connect4 with valid type ... OK
  Test case #1 (IPv4/TCP):
          Requested: bind(192.168.1.254, 4040) ..
             Actual: bind(127.0.0.1, 4444)
          Requested: connect(192.168.1.254, 4040) from (*, *) ..
             Actual: connect(127.0.0.1, 4444) from (127.0.0.4, 56068)
  Test case #2 (IPv4/UDP):
          Requested: bind(192.168.1.254, 4040) ..
             Actual: bind(127.0.0.1, 4444)
          Requested: connect(192.168.1.254, 4040) from (*, *) ..
             Actual: connect(127.0.0.1, 4444) from (127.0.0.4, 56447)
  Load bind6 with invalid type (can pollute stderr) ... REJECTED
  Load bind6 with valid type ... OK
  Attach bind6 with invalid type ... REJECTED
  Attach bind6 with valid type ... OK
  Load connect6 with invalid type (can pollute stderr) libbpf: load bpf \
    program failed: Permission denied
  libbpf: -- BEGIN DUMP LOG ---
  libbpf:
  0: (b7) r6 = 0
  1: (63) *(u32 *)(r1 +12) = r6
  invalid bpf_context access off=12 size=4

  libbpf: -- END LOG --
  libbpf: failed to load program 'cgroup/connect6'
  libbpf: failed to load object './connect6_prog.o'
  ... REJECTED
  Load connect6 with valid type ... OK
  Attach connect6 with invalid type ... REJECTED
  Attach connect6 with valid type ... OK
  Test case #3 (IPv6/TCP):
          Requested: bind(face:b00c:1234:5678::abcd, 6060) ..
             Actual: bind(::1, 6666)
          Requested: connect(face:b00c:1234:5678::abcd, 6060) from (*, *)
             Actual: connect(::1, 6666) from (::6, 37458)
  Test case #4 (IPv6/UDP):
          Requested: bind(face:b00c:1234:5678::abcd, 6060) ..
             Actual: bind(::1, 6666)
          Requested: connect(face:b00c:1234:5678::abcd, 6060) from (*, *)
             Actual: connect(::1, 6666) from (::6, 39315)
  ### SUCCESS
  # egrep 'connect\(.*AF_INET' connect.trace | \
  > egrep -vw 'htons\(1025\)' | fold -b -s -w 72
  502   connect(7, {sa_family=AF_INET, sin_port=htons(4040),
  sin_addr=inet_addr("192.168.1.254")}, 128) = 0
  502   connect(8, {sa_family=AF_INET, sin_port=htons(4040),
  sin_addr=inet_addr("192.168.1.254")}, 128) = 0
  502   connect(9, {sa_family=AF_INET6, sin6_port=htons(6060),
  inet_pton(AF_INET6, "face:b00c:1234:5678::abcd", &sin6_addr),
  sin6_flowinfo=0, sin6_scope_id=0}, 128) = 0
  502   connect(10, {sa_family=AF_INET6, sin6_port=htons(6060),
  inet_pton(AF_INET6, "face:b00c:1234:5678::abcd", &sin6_addr),
  sin6_flowinfo=0, sin6_scope_id=0}, 128) = 0
  # fg
  tcpdump -pn -i lo -w connect.pcap 2> /dev/null
  # tcpdump -r connect.pcap -n tcp | cut -c 1-72
  reading from file connect.pcap, link-type EN10MB (Ethernet)
  17:57:40.383533 IP 127.0.0.4.56068 > 127.0.0.1.4444: Flags [S], seq 1333
  17:57:40.383566 IP 127.0.0.1.4444 > 127.0.0.4.56068: Flags [S.], seq 112
  17:57:40.383589 IP 127.0.0.4.56068 > 127.0.0.1.4444: Flags [.], ack 1, w
  17:57:40.384578 IP 127.0.0.1.4444 > 127.0.0.4.56068: Flags [R.], seq 1,
  17:57:40.403327 IP6 ::6.37458 > ::1.6666: Flags [S], seq 406513443, win
  17:57:40.403357 IP6 ::1.6666 > ::6.37458: Flags [S.], seq 2448389240, ac
  17:57:40.403376 IP6 ::6.37458 > ::1.6666: Flags [.], ack 1, win 342, opt
  17:57:40.404263 IP6 ::1.6666 > ::6.37458: Flags [R.], seq 1, ack 1, win

Signed-off-by: Andrey Ignatov <rdna@fb.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
tools/include/uapi/linux/bpf.h
tools/lib/bpf/libbpf.c
tools/testing/selftests/bpf/Makefile
tools/testing/selftests/bpf/bpf_helpers.h
tools/testing/selftests/bpf/connect4_prog.c [new file with mode: 0644]
tools/testing/selftests/bpf/connect6_prog.c [new file with mode: 0644]
tools/testing/selftests/bpf/test_sock_addr.c
tools/testing/selftests/bpf/test_sock_addr.sh [new file with mode: 0755]

index f2120c5..71051d0 100644 (file)
@@ -150,6 +150,8 @@ enum bpf_attach_type {
        BPF_SK_MSG_VERDICT,
        BPF_CGROUP_INET4_BIND,
        BPF_CGROUP_INET6_BIND,
+       BPF_CGROUP_INET4_CONNECT,
+       BPF_CGROUP_INET6_CONNECT,
        __MAX_BPF_ATTACH_TYPE
 };
 
@@ -744,6 +746,13 @@ union bpf_attr {
  *     @flags: reserved for future use
  *     Return: SK_PASS
  *
+ * int bpf_bind(ctx, addr, addr_len)
+ *     Bind socket to address. Only binding to IP is supported, no port can be
+ *     set in addr.
+ *     @ctx: pointer to context of type bpf_sock_addr
+ *     @addr: pointer to struct sockaddr to bind socket to
+ *     @addr_len: length of sockaddr structure
+ *     Return: 0 on success or negative error code
  */
 #define __BPF_FUNC_MAPPER(FN)          \
        FN(unspec),                     \
@@ -809,7 +818,8 @@ union bpf_attr {
        FN(msg_redirect_map),           \
        FN(msg_apply_bytes),            \
        FN(msg_cork_bytes),             \
-       FN(msg_pull_data),
+       FN(msg_pull_data),              \
+       FN(bind),
 
 /* integer value in 'imm' field of BPF_CALL instruction selects which helper
  * function eBPF program intends to call
index d7ce881..5922443 100644 (file)
@@ -1887,6 +1887,8 @@ static const struct {
        BPF_PROG_SEC("sk_msg",          BPF_PROG_TYPE_SK_MSG),
        BPF_SA_PROG_SEC("cgroup/bind4", BPF_CGROUP_INET4_BIND),
        BPF_SA_PROG_SEC("cgroup/bind6", BPF_CGROUP_INET6_BIND),
+       BPF_SA_PROG_SEC("cgroup/connect4", BPF_CGROUP_INET4_CONNECT),
+       BPF_SA_PROG_SEC("cgroup/connect6", BPF_CGROUP_INET6_CONNECT),
 };
 
 #undef BPF_PROG_SEC
index f4717c9..c64d4eb 100644 (file)
@@ -30,14 +30,15 @@ TEST_GEN_FILES = test_pkt_access.o test_xdp.o test_l4lb.o test_tcp_estats.o test
        sockmap_verdict_prog.o dev_cgroup.o sample_ret0.o test_tracepoint.o \
        test_l4lb_noinline.o test_xdp_noinline.o test_stacktrace_map.o \
        sample_map_ret0.o test_tcpbpf_kern.o test_stacktrace_build_id.o \
-       sockmap_tcp_msg_prog.o
+       sockmap_tcp_msg_prog.o connect4_prog.o connect6_prog.o
 
 # Order correspond to 'make run_tests' order
 TEST_PROGS := test_kmod.sh \
        test_libbpf.sh \
        test_xdp_redirect.sh \
        test_xdp_meta.sh \
-       test_offload.py
+       test_offload.py \
+       test_sock_addr.sh
 
 # Compile but not part of 'make run_tests'
 TEST_GEN_PROGS_EXTENDED = test_libbpf_open
index 7cae376..d8223d9 100644 (file)
@@ -94,6 +94,8 @@ static int (*bpf_msg_cork_bytes)(void *ctx, int len) =
        (void *) BPF_FUNC_msg_cork_bytes;
 static int (*bpf_msg_pull_data)(void *ctx, int start, int end, int flags) =
        (void *) BPF_FUNC_msg_pull_data;
+static int (*bpf_bind)(void *ctx, void *addr, int addr_len) =
+       (void *) BPF_FUNC_bind;
 
 /* llvm builtin functions that eBPF C program may use to
  * emit BPF_LD_ABS and BPF_LD_IND instructions
diff --git a/tools/testing/selftests/bpf/connect4_prog.c b/tools/testing/selftests/bpf/connect4_prog.c
new file mode 100644 (file)
index 0000000..5a88a68
--- /dev/null
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (c) 2018 Facebook
+
+#include <string.h>
+
+#include <linux/stddef.h>
+#include <linux/bpf.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <sys/socket.h>
+
+#include "bpf_helpers.h"
+#include "bpf_endian.h"
+
+#define SRC_REWRITE_IP4                0x7f000004U
+#define DST_REWRITE_IP4                0x7f000001U
+#define DST_REWRITE_PORT4      4444
+
+int _version SEC("version") = 1;
+
+SEC("cgroup/connect4")
+int connect_v4_prog(struct bpf_sock_addr *ctx)
+{
+       struct sockaddr_in sa;
+
+       /* Rewrite destination. */
+       ctx->user_ip4 = bpf_htonl(DST_REWRITE_IP4);
+       ctx->user_port = bpf_htons(DST_REWRITE_PORT4);
+
+       if (ctx->type == SOCK_DGRAM || ctx->type == SOCK_STREAM) {
+               ///* Rewrite source. */
+               memset(&sa, 0, sizeof(sa));
+
+               sa.sin_family = AF_INET;
+               sa.sin_port = bpf_htons(0);
+               sa.sin_addr.s_addr = bpf_htonl(SRC_REWRITE_IP4);
+
+               if (bpf_bind(ctx, (struct sockaddr *)&sa, sizeof(sa)) != 0)
+                       return 0;
+       }
+
+       return 1;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/connect6_prog.c b/tools/testing/selftests/bpf/connect6_prog.c
new file mode 100644 (file)
index 0000000..8ea3f7d
--- /dev/null
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (c) 2018 Facebook
+
+#include <string.h>
+
+#include <linux/stddef.h>
+#include <linux/bpf.h>
+#include <linux/in.h>
+#include <linux/in6.h>
+#include <sys/socket.h>
+
+#include "bpf_helpers.h"
+#include "bpf_endian.h"
+
+#define SRC_REWRITE_IP6_0      0
+#define SRC_REWRITE_IP6_1      0
+#define SRC_REWRITE_IP6_2      0
+#define SRC_REWRITE_IP6_3      6
+
+#define DST_REWRITE_IP6_0      0
+#define DST_REWRITE_IP6_1      0
+#define DST_REWRITE_IP6_2      0
+#define DST_REWRITE_IP6_3      1
+
+#define DST_REWRITE_PORT6      6666
+
+int _version SEC("version") = 1;
+
+SEC("cgroup/connect6")
+int connect_v6_prog(struct bpf_sock_addr *ctx)
+{
+       struct sockaddr_in6 sa;
+
+       /* Rewrite destination. */
+       ctx->user_ip6[0] = bpf_htonl(DST_REWRITE_IP6_0);
+       ctx->user_ip6[1] = bpf_htonl(DST_REWRITE_IP6_1);
+       ctx->user_ip6[2] = bpf_htonl(DST_REWRITE_IP6_2);
+       ctx->user_ip6[3] = bpf_htonl(DST_REWRITE_IP6_3);
+
+       ctx->user_port = bpf_htons(DST_REWRITE_PORT6);
+
+       if (ctx->type == SOCK_DGRAM || ctx->type == SOCK_STREAM) {
+               /* Rewrite source. */
+               memset(&sa, 0, sizeof(sa));
+
+               sa.sin6_family = AF_INET6;
+               sa.sin6_port = bpf_htons(0);
+
+               sa.sin6_addr.s6_addr32[0] = bpf_htonl(SRC_REWRITE_IP6_0);
+               sa.sin6_addr.s6_addr32[1] = bpf_htonl(SRC_REWRITE_IP6_1);
+               sa.sin6_addr.s6_addr32[2] = bpf_htonl(SRC_REWRITE_IP6_2);
+               sa.sin6_addr.s6_addr32[3] = bpf_htonl(SRC_REWRITE_IP6_3);
+
+               if (bpf_bind(ctx, (struct sockaddr *)&sa, sizeof(sa)) != 0)
+                       return 0;
+       }
+
+       return 1;
+}
+
+char _license[] SEC("license") = "GPL";
index a57e13a..d488f20 100644 (file)
 #include <linux/filter.h>
 
 #include <bpf/bpf.h>
+#include <bpf/libbpf.h>
 
 #include "cgroup_helpers.h"
 
 #define CG_PATH        "/foo"
+#define CONNECT4_PROG_PATH     "./connect4_prog.o"
+#define CONNECT6_PROG_PATH     "./connect6_prog.o"
 
 #define SERV4_IP               "192.168.1.254"
 #define SERV4_REWRITE_IP       "127.0.0.1"
@@ -254,6 +257,41 @@ static int bind6_prog_load(enum bpf_attach_type attach_type,
                          sizeof(insns) / sizeof(struct bpf_insn), comment);
 }
 
+static int connect_prog_load_path(const char *path,
+                                 enum bpf_attach_type attach_type,
+                                 const char *comment)
+{
+       struct bpf_prog_load_attr attr;
+       struct bpf_object *obj;
+       int prog_fd;
+
+       memset(&attr, 0, sizeof(struct bpf_prog_load_attr));
+       attr.file = path;
+       attr.prog_type = BPF_PROG_TYPE_CGROUP_SOCK_ADDR;
+       attr.expected_attach_type = attach_type;
+
+       if (bpf_prog_load_xattr(&attr, &obj, &prog_fd)) {
+               if (comment)
+                       log_err(">>> Loading %s program at %s error.\n",
+                               comment, path);
+               return -1;
+       }
+
+       return prog_fd;
+}
+
+static int connect4_prog_load(enum bpf_attach_type attach_type,
+                             const char *comment)
+{
+       return connect_prog_load_path(CONNECT4_PROG_PATH, attach_type, comment);
+}
+
+static int connect6_prog_load(enum bpf_attach_type attach_type,
+                             const char *comment)
+{
+       return connect_prog_load_path(CONNECT6_PROG_PATH, attach_type, comment);
+}
+
 static void print_ip_port(int sockfd, info_fn fn, const char *fmt)
 {
        char addr_buf[INET_NTOP_BUF];
@@ -290,6 +328,11 @@ static void print_local_ip_port(int sockfd, const char *fmt)
        print_ip_port(sockfd, getsockname, fmt);
 }
 
+static void print_remote_ip_port(int sockfd, const char *fmt)
+{
+       print_ip_port(sockfd, getpeername, fmt);
+}
+
 static int start_server(int type, const struct sockaddr_storage *addr,
                        socklen_t addr_len)
 {
@@ -324,6 +367,39 @@ out:
        return fd;
 }
 
+static int connect_to_server(int type, const struct sockaddr_storage *addr,
+                            socklen_t addr_len)
+{
+       int domain;
+       int fd;
+
+       domain = addr->ss_family;
+
+       if (domain != AF_INET && domain != AF_INET6) {
+               log_err("Unsupported address family");
+               return -1;
+       }
+
+       fd = socket(domain, type, 0);
+       if (fd == -1) {
+               log_err("Failed to creating client socket");
+               return -1;
+       }
+
+       if (connect(fd, (const struct sockaddr *)addr, addr_len) == -1) {
+               log_err("Fail to connect to server");
+               goto err;
+       }
+
+       print_remote_ip_port(fd, "\t   Actual: connect(%s, %d)");
+       print_local_ip_port(fd, " from (%s, %d)\n");
+
+       return 0;
+err:
+       close(fd);
+       return -1;
+}
+
 static void print_test_case_num(int domain, int type)
 {
        static int test_num;
@@ -356,6 +432,10 @@ static int run_test_case(int domain, int type, const char *ip,
        if (servfd == -1)
                goto err;
 
+       printf("\tRequested: connect(%s, %d) from (*, *) ..\n", ip, port);
+       if (connect_to_server(type, &addr, addr_len))
+               goto err;
+
        goto out;
 err:
        err = -1;
@@ -380,29 +460,41 @@ static int load_and_attach_progs(int cgfd, struct program *progs,
        size_t i;
 
        for (i = 0; i < prog_cnt; ++i) {
+               printf("Load %s with invalid type (can pollute stderr) ",
+                      progs[i].name);
+               fflush(stdout);
                progs[i].fd = progs[i].loadfn(progs[i].invalid_type, NULL);
                if (progs[i].fd != -1) {
                        log_err("Load with invalid type accepted for %s",
                                progs[i].name);
                        goto err;
                }
+               printf("... REJECTED\n");
+
+               printf("Load %s with valid type", progs[i].name);
                progs[i].fd = progs[i].loadfn(progs[i].type, progs[i].name);
                if (progs[i].fd == -1) {
                        log_err("Failed to load program %s", progs[i].name);
                        goto err;
                }
+               printf(" ... OK\n");
+
+               printf("Attach %s with invalid type", progs[i].name);
                if (bpf_prog_attach(progs[i].fd, cgfd, progs[i].invalid_type,
                                    BPF_F_ALLOW_OVERRIDE) != -1) {
                        log_err("Attach with invalid type accepted for %s",
                                progs[i].name);
                        goto err;
                }
+               printf(" ... REJECTED\n");
+
+               printf("Attach %s with valid type", progs[i].name);
                if (bpf_prog_attach(progs[i].fd, cgfd, progs[i].type,
                                    BPF_F_ALLOW_OVERRIDE) == -1) {
                        log_err("Failed to attach program %s", progs[i].name);
                        goto err;
                }
-               printf("Attached %s program.\n", progs[i].name);
+               printf(" ... OK\n");
        }
 
        return 0;
@@ -443,12 +535,16 @@ static int run_test(void)
        struct program inet6_progs[] = {
                {BPF_CGROUP_INET6_BIND, bind6_prog_load, -1, "bind6",
                 BPF_CGROUP_INET4_BIND},
+               {BPF_CGROUP_INET6_CONNECT, connect6_prog_load, -1, "connect6",
+                BPF_CGROUP_INET4_CONNECT},
        };
        inet6_prog_cnt = sizeof(inet6_progs) / sizeof(struct program);
 
        struct program inet_progs[] = {
                {BPF_CGROUP_INET4_BIND, bind4_prog_load, -1, "bind4",
                 BPF_CGROUP_INET6_BIND},
+               {BPF_CGROUP_INET4_CONNECT, connect4_prog_load, -1, "connect4",
+                BPF_CGROUP_INET6_CONNECT},
        };
        inet_prog_cnt = sizeof(inet_progs) / sizeof(struct program);
 
@@ -482,5 +578,11 @@ out:
 
 int main(int argc, char **argv)
 {
+       if (argc < 2) {
+               fprintf(stderr,
+                       "%s has to be run via %s.sh. Skip direct run.\n",
+                       argv[0], argv[0]);
+               exit(0);
+       }
        return run_test();
 }
diff --git a/tools/testing/selftests/bpf/test_sock_addr.sh b/tools/testing/selftests/bpf/test_sock_addr.sh
new file mode 100755 (executable)
index 0000000..c6e1dcf
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+set -eu
+
+ping_once()
+{
+       ping -q -c 1 -W 1 ${1%%/*} >/dev/null 2>&1
+}
+
+wait_for_ip()
+{
+       local _i
+       echo -n "Wait for testing IPv4/IPv6 to become available "
+       for _i in $(seq ${MAX_PING_TRIES}); do
+               echo -n "."
+               if ping_once ${TEST_IPv4} && ping_once ${TEST_IPv6}; then
+                       echo " OK"
+                       return
+               fi
+       done
+       echo 1>&2 "ERROR: Timeout waiting for test IP to become available."
+       exit 1
+}
+
+setup()
+{
+       # Create testing interfaces not to interfere with current environment.
+       ip link add dev ${TEST_IF} type veth peer name ${TEST_IF_PEER}
+       ip link set ${TEST_IF} up
+       ip link set ${TEST_IF_PEER} up
+
+       ip -4 addr add ${TEST_IPv4} dev ${TEST_IF}
+       ip -6 addr add ${TEST_IPv6} dev ${TEST_IF}
+       wait_for_ip
+}
+
+cleanup()
+{
+       ip link del ${TEST_IF} 2>/dev/null || :
+       ip link del ${TEST_IF_PEER} 2>/dev/null || :
+}
+
+main()
+{
+       trap cleanup EXIT 2 3 6 15
+       setup
+       ./test_sock_addr setup_done
+}
+
+BASENAME=$(basename $0 .sh)
+TEST_IF="${BASENAME}1"
+TEST_IF_PEER="${BASENAME}2"
+TEST_IPv4="127.0.0.4/8"
+TEST_IPv6="::6/128"
+MAX_PING_TRIES=5
+
+main