This patch adds two BPF conntrack helper functions, bpf_ct_lookup()
and bpf_ct_commit(), to enable the possibility of BPF stateful firewall.

There are two ways to implement BPF conntrack.  One way is to not
rely on helpers but implement the conntrack state table using BPF
maps.  So conntrack is basically another BPF program extracting
the tuples and lookup/update its map.  Currenly Cillium project has
implemented this way.

Instaed, this patch is proposed to reuse the the existing netfilter
conntrack.  By creating two helpers, bpf_skb_ct_lookup() and
bpf_skb_ct_commit(), a BPF program simply lookup the conntrack state,
based on the conntrack zone, or commit the new connection into the
netfilter conntrack table, for later lookup hit.  In the case where
a fragmented packet is received, the helper handles it by submitting
packets to ip_defrag() and return TC_ACT_STOLEN if being queued.

An simple example is to allow PING packet to your host, only when
your host initiates the ping.  The first ping from your host to
outside will be tracked into conntrack table as new, and the echo
reply coming into your host will make it established.  So the
subsequent ping will pass the firwall. (see tcbpf3_kern.c and
test_conntrack_bpf.h)

Now the helper is attached to TC, and requires skb to interact with
netfilter conntrack, so XDP program does not apply here.
In the future, I'd like to test/implement ip defragmentation, alg,
and nat.  But I'd like to have some early feedbacks. Thanks!

Signed-off-by: William Tu <[email protected]>
Cc: Joe Stringer <[email protected]>
Cc: Daniel Borkmann <[email protected]>
Cc: Alexei Starovoitov <[email protected]>
---
 include/uapi/linux/bpf.h                  |  31 +++++
 net/core/filter.c                         | 224 ++++++++++++++++++++++++++++++
 samples/bpf/Makefile                      |   1 +
 samples/bpf/tcbpf3_kern.c                 | 122 ++++++++++++++++
 samples/bpf/test_conntrack_bpf.sh         |  21 +++
 tools/testing/selftests/bpf/bpf_helpers.h |   5 +-
 6 files changed, 403 insertions(+), 1 deletion(-)
 create mode 100644 samples/bpf/tcbpf3_kern.c
 create mode 100755 samples/bpf/test_conntrack_bpf.sh

diff --git a/include/uapi/linux/bpf.h b/include/uapi/linux/bpf.h
index 08c206a863e1..b8c70012a56f 100644
--- a/include/uapi/linux/bpf.h
+++ b/include/uapi/linux/bpf.h
@@ -582,6 +582,18 @@ union bpf_attr {
  *     @map: pointer to sockmap to update
  *     @key: key to insert/update sock in map
  *     @flags: same flags as map update elem
+ *
+ * int bpf_skb_ct_lookup(skb, info, flags)
+ *     @skb: pointer to skb
+ *     @info: pointer to 'struct bpf_conntrack_info'
+ *     @flags: reserved for future use
+ *     Return: 0 on success or negative error
+ *
+ * int bpf_skb_ct_commit(skb, info, flags)
+ *     @skb: pointer to skb
+ *     @info: pointer to 'struct bpf_conntrack_info'
+ *     @flags: reserved for future use
+ *     Return: 0 on success or negative error
  */
 #define __BPF_FUNC_MAPPER(FN)          \
        FN(unspec),                     \
@@ -638,6 +650,8 @@ union bpf_attr {
        FN(redirect_map),               \
        FN(sk_redirect_map),            \
        FN(sock_map_update),            \
+       FN(skb_ct_lookup),              \
+       FN(skb_ct_commit),              \
 
 /* integer value in 'imm' field of BPF_CALL instruction selects which helper
  * function eBPF program intends to call
@@ -737,6 +751,23 @@ struct bpf_tunnel_key {
        __u32 tunnel_label;
 };
 
+#define BPF_CT_LABELS_LEN 16
+#define BPF_F_CT_DEFRAG                (1ULL << 0)
+
+struct bpf_conntrack_info {
+       __u32 ct_state;
+       __u16 family;
+       __u16 zone_id;
+
+       /* for conntrack mark */
+       __u32 mark_value;
+       __u32 mark_mask;
+
+       /* for conntrack label */
+       __u8 ct_label_value[16];
+       __u8 ct_label_mask[16];
+};
+
 /* Generic BPF return codes which all BPF program types may support.
  * The values are binary compatible with their TC_ACT_* counter-part to
  * provide backwards compatibility with existing SCHED_CLS and SCHED_ACT
diff --git a/net/core/filter.c b/net/core/filter.c
index c6a37fe0285b..906518043f19 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -56,6 +56,11 @@
 #include <net/busy_poll.h>
 #include <net/tcp.h>
 #include <linux/bpf_trace.h>
+#include <net/netfilter/nf_conntrack_core.h>
+#include <net/netfilter/nf_conntrack.h>
+#include <net/netfilter/nf_conntrack_helper.h>
+#include <net/netfilter/nf_conntrack_labels.h>
+#include <net/netfilter/nf_conntrack_zones.h>
 
 /**
  *     sk_filter_trim_cap - run a packet through a socket filter
@@ -2674,6 +2679,221 @@ static unsigned long bpf_skb_copy(void *dst_buff, const 
void *skb,
        return 0;
 }
 
+/* Lookup the conntrack with bpf_conntrack_info.
+ * If found, receive the conntrack state, mark, and label
+ */
+BPF_CALL_3(bpf_skb_ct_lookup, struct sk_buff *, skb,
+          struct bpf_conntrack_info *, info, u64, flags)
+{
+       struct net *net = dev_net(skb->dev);
+       enum ip_conntrack_info ctinfo;
+       struct nf_conntrack_zone zone;
+       struct nf_conn_labels *cl;
+       struct nf_conn *ct, *tmpl;
+       int err, nh_ofs;
+       struct iphdr *iph;
+
+       /* Conntrack expects L3 packet */
+       nh_ofs = skb_network_offset(skb);
+       skb_pull_rcsum(skb, nh_ofs);
+       iph = ip_hdr(skb);
+
+       if (ip_is_fragment(iph) && (flags & BPF_F_CT_DEFRAG)) {
+               /* XXX: not test yet */
+               enum ip_defrag_users user =
+                       IP_DEFRAG_CONNTRACK_IN + info->zone_id;
+
+               memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
+               err = ip_defrag(net, skb, user);
+               if (err == -EINPROGRESS)
+                       return TC_ACT_STOLEN;
+       }
+
+       /* TODO: conntrack expectation */
+
+       nf_ct_zone_init(&zone, info->zone_id,
+                       NF_CT_DEFAULT_ZONE_DIR, 0);
+       tmpl = nf_ct_tmpl_alloc(net, &zone, GFP_KERNEL);
+       if (!tmpl) {
+               pr_err("Failed to allocate conntrack template\n");
+               goto error;
+       }
+       nf_conntrack_get(&tmpl->ct_general);
+       nf_ct_set(skb, tmpl, IP_CT_NEW);
+
+       do {
+               err = nf_conntrack_in(net, info->family,
+                                     NF_INET_PRE_ROUTING, skb);
+       } while (err != NF_ACCEPT);
+
+       ct = nf_ct_get(skb, &ctinfo);
+       if (!ct) {
+               pr_err("nf_ct_get failed\n");
+               goto error;
+       }
+
+       info->ct_state = (u8)ctinfo;
+       info->mark_value = ct->mark;
+
+       cl = nf_ct_labels_find(ct);
+       if (cl) {
+               size_t len = sizeof(cl->bits);
+
+               if (len > BPF_CT_LABELS_LEN)
+                       len = BPF_CT_LABELS_LEN;
+               else if (len < BPF_CT_LABELS_LEN)
+                       memset(info->ct_label_value, 0, BPF_CT_LABELS_LEN);
+               memcpy(info->ct_label_value, cl->bits, len);
+       } else {
+               memset(info->ct_label_value, 0, BPF_CT_LABELS_LEN);
+       }
+
+error:
+       skb_push_rcsum(skb, nh_ofs);
+       return TC_ACT_OK;
+}
+
+static const struct bpf_func_proto bpf_skb_ct_lookup_proto = {
+       .func           = bpf_skb_ct_lookup,
+       .gpl_only       = true,
+       .ret_type       = RET_INTEGER,
+       .arg1_type      = ARG_PTR_TO_CTX,
+       .arg2_type      = ARG_PTR_TO_MEM,
+       .arg3_type      = ARG_ANYTHING,
+};
+
+static int bpf_skb_ct_set_mark(struct sk_buff *skb, u32 ct_mark, u32 mask)
+{
+#if IS_ENABLED(CONFIG_NF_CONNTRACK_MARK)
+       enum ip_conntrack_info ctinfo;
+       struct nf_conn *ct;
+       u32 new_mark;
+
+       ct = nf_ct_get(skb, &ctinfo);
+       if (!ct)
+               return 0;
+
+       new_mark = ct_mark | (ct->mark & ~(mask));
+       if (ct->mark != new_mark)
+               ct->mark = new_mark;
+       return 0;
+#else
+       return -ENOTSUP;
+#endif
+}
+
+static int bpf_skb_ct_set_labels(struct sk_buff *skb,
+                                const u8 *labels, const u8 *mask)
+{
+#if IS_ENABLED(CONFIG_NF_CONNTRACK_LABELS)
+       enum ip_conntrack_info ctinfo;
+       struct nf_conn_labels *cl;
+       struct nf_conn *ct;
+       int err;
+
+       ct = nf_ct_get(skb, &ctinfo);
+       if (!ct)
+               return 0;
+
+       cl = nf_ct_labels_find(ct);
+       if (!cl) {
+               nf_ct_labels_ext_add(ct);
+               cl = nf_ct_labels_find(ct);
+               if (!cl)
+                       return -ENOSPC;
+       }
+
+       err = nf_connlabels_replace(ct, (u32 *)labels, (u32 *)mask,
+                                   BPF_CT_LABELS_LEN / sizeof(u32));
+       if (err)
+               return err;
+       return 0;
+#else
+       return -ENOTSUP;
+#endif
+}
+
+static bool labels_nonzero(const u8 *labels)
+{
+       int i;
+
+       for (i = 0; i < BPF_CT_LABELS_LEN; i++)
+               if (labels[i])
+                       return true;
+       return false;
+}
+
+/* Take the skb and do nf_conntrack_in first,
+ * set mark and labels, and confirm
+ */
+BPF_CALL_3(bpf_skb_ct_commit, struct sk_buff *, skb,
+          struct bpf_conntrack_info *, info, u64, flags)
+{
+       struct net *net = dev_net(skb->dev);
+       enum ip_conntrack_info ctinfo;
+       struct nf_conntrack_zone zone;
+       struct nf_conn *ct, *tmpl;
+       int err, nh_ofs;
+
+       /* Conntrack expects L3 packet */
+       nh_ofs = skb_network_offset(skb);
+       skb_pull_rcsum(skb, nh_ofs);
+
+       nf_ct_zone_init(&zone, info->zone_id,
+                       NF_CT_DEFAULT_ZONE_DIR, 0);
+       tmpl = nf_ct_tmpl_alloc(net, &zone, GFP_KERNEL);
+       if (!tmpl) {
+               pr_err("Failed to allocate conntrack template\n");
+               goto error;
+       }
+
+       nf_conntrack_get(&tmpl->ct_general);
+       nf_ct_set(skb, tmpl, IP_CT_NEW);
+
+       do {
+               err = nf_conntrack_in(net, info->family,
+                                     NF_INET_PRE_ROUTING, skb);
+       } while (err != NF_ACCEPT);
+
+       ct = nf_ct_get(skb, &ctinfo);
+       if (!ct) {
+               pr_err("nf_ct_get failed\n");
+               goto error;
+       }
+
+       if (info->mark_mask) {
+               err = bpf_skb_ct_set_mark(skb, info->mark_value,
+                                         info->mark_mask);
+               if (err)
+                       goto error;
+       }
+
+       if (labels_nonzero(info->ct_label_mask)) {
+               err = bpf_skb_ct_set_labels(skb, info->ct_label_value,
+                                           info->ct_label_mask);
+               if (err)
+                       goto error;
+       }
+
+       /* place into conntrack hash table. */
+       if (nf_conntrack_confirm(skb) != NF_ACCEPT) {
+               pr_err("Failed to confirm\n");
+               goto error;
+       }
+error:
+       skb_push_rcsum(skb, nh_ofs);
+       return TC_ACT_OK;
+}
+
+static const struct bpf_func_proto bpf_skb_ct_commit_proto = {
+       .func           = bpf_skb_ct_commit,
+       .gpl_only       = true,
+       .ret_type       = RET_INTEGER,
+       .arg1_type      = ARG_PTR_TO_CTX,
+       .arg2_type      = ARG_PTR_TO_MEM,
+       .arg3_type      = ARG_ANYTHING,
+};
+
 BPF_CALL_5(bpf_skb_event_output, struct sk_buff *, skb, struct bpf_map *, map,
           u64, flags, void *, meta, u64, meta_size)
 {
@@ -3226,6 +3446,10 @@ tc_cls_act_func_proto(enum bpf_func_id func_id)
                return &bpf_get_socket_cookie_proto;
        case BPF_FUNC_get_socket_uid:
                return &bpf_get_socket_uid_proto;
+       case BPF_FUNC_skb_ct_lookup:
+               return &bpf_skb_ct_lookup_proto;
+       case BPF_FUNC_skb_ct_commit:
+               return &bpf_skb_ct_commit_proto;
        default:
                return bpf_base_func_proto(func_id);
        }
diff --git a/samples/bpf/Makefile b/samples/bpf/Makefile
index cf17c7932a6e..f38e480bfdd3 100644
--- a/samples/bpf/Makefile
+++ b/samples/bpf/Makefile
@@ -103,6 +103,7 @@ always += test_probe_write_user_kern.o
 always += trace_output_kern.o
 always += tcbpf1_kern.o
 always += tcbpf2_kern.o
+always += tcbpf3_kern.o
 always += tc_l2_redirect_kern.o
 always += lathist_kern.o
 always += offwaketime_kern.o
diff --git a/samples/bpf/tcbpf3_kern.c b/samples/bpf/tcbpf3_kern.c
new file mode 100644
index 000000000000..ca3104ce7471
--- /dev/null
+++ b/samples/bpf/tcbpf3_kern.c
@@ -0,0 +1,122 @@
+/* Copyright (c) 2016 VMware
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of version 2 of the GNU General Public
+ * License as published by the Free Software Foundation.
+ */
+#define KBUILD_MODNAME "foo"
+#include <uapi/linux/bpf.h>
+#include <uapi/linux/if_ether.h>
+#include <uapi/linux/if_packet.h>
+#include <uapi/linux/ip.h>
+#include <uapi/linux/ipv6.h>
+#include <uapi/linux/in.h>
+#include <uapi/linux/tcp.h>
+#include <uapi/linux/filter.h>
+#include <uapi/linux/pkt_cls.h>
+#include <net/ipv6.h>
+#include "bpf_helpers.h"
+#include "bpf_endian.h"
+
+#define printk(fmt, ...)    \
+({  char ___fmt[] = fmt;    \
+       bpf_trace_printk(___fmt, sizeof(___fmt), ##__VA_ARGS__);\
+})
+
+//#include <net/netfilter/nf_conntrack_common.h>
+#define IP_CT_ESTABLISHED      0
+#define IP_CT_RELATED          1
+#define IP_CT_NEW              2
+
+/* Example of a stateful firewall using BPF conntrack
+ * - Only allow PING from the connections originates
+ *   from this host (zone 0x10)
+ * - Attach ct_commit at tc egress, tracking the connection originating
+ *   from this host.
+ * - Attach ct_lookup at tc ingress, pass when ctinfo == ESTABLISHED,
+ *   otherwise drop.
+ */
+
+SEC("ct_lookup")
+int _ct_lookup(struct __sk_buff *skb)
+{
+       int ret, flags;
+       __u64 proto;
+       struct bpf_conntrack_info info;
+
+       proto = load_half(skb, 12);
+       if (proto != ETH_P_IP) {
+               return TC_ACT_OK;
+       } else {
+               __u64 ip_proto = load_byte(skb, 14 +
+                                          offsetof(struct iphdr, protocol));
+               if (ip_proto != IPPROTO_ICMP)
+                       return TC_ACT_OK;
+       }
+       /* Only process ICMP */
+
+       __builtin_memset(&info, 0x0, sizeof(info));
+       info.zone_id = 0x10;
+       info.family = NFPROTO_IPV4;
+
+       ret = bpf_skb_ct_lookup(skb, &info, 0);
+       if (ret < 0) {
+               printk("ct_lookup failed\n");
+               return TC_ACT_OK;
+       }
+
+       printk("ct_lookup: zone: %d state: %d mark: %x",
+               info.zone_id, info.ct_state, info.mark_value);
+
+       if (info.ct_state == IP_CT_ESTABLISHED) {
+               printk("allow established connection\n");
+               return TC_ACT_OK;
+       } else if (info.ct_state == IP_CT_NEW) {
+               printk("drop new connection\n");
+               return TC_ACT_SHOT;
+       } else {
+               return TC_ACT_OK;
+       }
+
+       return TC_ACT_OK;
+}
+
+SEC("ct_commit")
+int _ct_commit(struct __sk_buff *skb)
+{
+       struct bpf_conntrack_info info;
+       int ret, flags;
+       __u64 proto;
+
+       proto = load_half(skb, 12);
+       if (proto != ETH_P_IP) {
+               return TC_ACT_OK;
+       } else {
+               __u64 ip_proto = load_byte(skb, 14 +
+                                          offsetof(struct iphdr, protocol));
+               if (ip_proto != IPPROTO_ICMP)
+                       return TC_ACT_OK;
+       }
+       /* Only process ICMP */
+        __builtin_memset(&info, 0x0, sizeof(info));
+       info.zone_id = 0x10;
+       info.family = NFPROTO_IPV4;
+       info.mark_value = 0x00009487;
+       info.mark_mask = 0x0000ffff;
+
+       /* commit this skb to conntrack
+        * so the other direction (icmp echo reply) can pass
+        */
+       ret = bpf_skb_ct_commit(skb, &info, 0);
+       if (ret < 0) {
+               printk("ct_commit failed\n");
+               return TC_ACT_OK;
+       }
+
+       printk("ct_commit: zone: %d state: %d mark: %x",
+               info.zone_id, info.ct_state, info.mark_value);
+
+       return TC_ACT_OK;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/samples/bpf/test_conntrack_bpf.sh 
b/samples/bpf/test_conntrack_bpf.sh
new file mode 100755
index 000000000000..09a7d531595e
--- /dev/null
+++ b/samples/bpf/test_conntrack_bpf.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+DEV=enp0s16
+
+tc qdisc del dev enp0s16 clsact
+rmmod nf_conntrack_ipv4
+rmmod nf_defrag_ipv4
+
+set -ex
+modprobe nf_defrag_ipv4
+modprobe nf_conntrack_ipv4
+
+echo 'module nf_conntrack +p' > /sys/kernel/debug/dynamic_debug/control
+echo 'module nf_conntrack_ipv4 +p' > /sys/kernel/debug/dynamic_debug/control
+echo 'module ip_fragment +p' > /sys/kernel/debug/dynamic_debug/control
+echo 'module icmp +p' > /sys/kernel/debug/dynamic_debug/control
+
+tc qdisc add dev enp0s16 clsact
+tc filter add dev enp0s16 ingress bpf da obj tcbpf3_kern.o sec ct_lookup
+tc filter add dev enp0s16  egress bpf da obj tcbpf3_kern.o sec ct_commit
+
+exit 0
diff --git a/tools/testing/selftests/bpf/bpf_helpers.h 
b/tools/testing/selftests/bpf/bpf_helpers.h
index 36fb9161b34a..b398ea57d10d 100644
--- a/tools/testing/selftests/bpf/bpf_helpers.h
+++ b/tools/testing/selftests/bpf/bpf_helpers.h
@@ -70,7 +70,10 @@ static int (*bpf_sk_redirect_map)(void *map, int key, int 
flags) =
 static int (*bpf_sock_map_update)(void *map, void *key, void *value,
                                  unsigned long long flags) =
        (void *) BPF_FUNC_sock_map_update;
-
+static int (*bpf_skb_ct_lookup)(void *ctx, void *ct, int flags) =
+       (void *) BPF_FUNC_skb_ct_lookup;
+static int (*bpf_skb_ct_commit)(void *ctx, void *ct, int flags) =
+       (void *) BPF_FUNC_skb_ct_commit;
 
 /* llvm builtin functions that eBPF C program may use to
  * emit BPF_LD_ABS and BPF_LD_IND instructions
-- 
2.7.4

_______________________________________________
iovisor-dev mailing list
[email protected]
https://lists.iovisor.org/mailman/listinfo/iovisor-dev

Reply via email to