diff --git a/bpf_linux.go b/bpf_linux.go new file mode 100644 index 0000000..320400a --- /dev/null +++ b/bpf_linux.go @@ -0,0 +1,53 @@ +package netlink + +/* +#include +#include +#include +#include + +static int load_simple_bpf(int prog_type) { + // { return 1; } + __u64 __attribute__((aligned(8))) insns[] = { + 0x00000001000000b7ull, + 0x0000000000000095ull, + }; + __u8 __attribute__((aligned(8))) license[] = "ASL2"; + // Copied from a header file since libc is notoriously slow to update. + // The call will succeed or fail and that will be our indication on + // whether or not it is supported. + struct { + __u32 prog_type; + __u32 insn_cnt; + __u64 insns; + __u64 license; + __u32 log_level; + __u32 log_size; + __u64 log_buf; + __u32 kern_version; + } __attribute__((aligned(8))) attr = { + .prog_type = prog_type, + .insn_cnt = 2, + .insns = (__u64)&insns, + .license = (__u64)&license, + }; + return syscall(__NR_bpf, 5, &attr, sizeof(attr)); +} +*/ +import "C" + +type BpfProgType C.int + +const ( + BPF_PROG_TYPE_UNSPEC BpfProgType = iota + BPF_PROG_TYPE_SOCKET_FILTER + BPF_PROG_TYPE_KPROBE + BPF_PROG_TYPE_SCHED_CLS + BPF_PROG_TYPE_SCHED_ACT +) + +// loadSimpleBpf loads a trivial bpf program for testing purposes +func loadSimpleBpf(progType BpfProgType) (int, error) { + fd, err := C.load_simple_bpf(C.int(progType)) + return int(fd), err +} diff --git a/filter.go b/filter.go index 80ef34d..5419bc3 100644 --- a/filter.go +++ b/filter.go @@ -26,11 +26,45 @@ func (q FilterAttrs) String() string { return fmt.Sprintf("{LinkIndex: %d, Handle: %s, Parent: %s, Priority: %d, Protocol: %d}", q.LinkIndex, HandleStr(q.Handle), HandleStr(q.Parent), q.Priority, q.Protocol) } +// Action represents an action in any supported filter. +type Action interface { + Type() string +} + +type BpfAction struct { + nl.TcActBpf + Fd int + Name string +} + +func (action *BpfAction) Type() string { + return "bpf" +} + +type MirredAction struct { + nl.TcMirred +} + +func (action *MirredAction) Type() string { + return "mirred" +} + +func NewMirredAction(redirIndex int) *MirredAction { + return &MirredAction{ + TcMirred: nl.TcMirred{ + TcGen: nl.TcGen{Action: nl.TC_ACT_STOLEN}, + Eaction: nl.TCA_EGRESS_REDIR, + Ifindex: uint32(redirIndex), + }, + } +} + // U32 filters on many packet related properties type U32 struct { FilterAttrs - // Currently only supports redirecting to another interface + ClassId uint32 RedirIndex int + Actions []Action } func (filter *U32) Attrs() *FilterAttrs { diff --git a/filter_linux.go b/filter_linux.go index 1dc688b..33f0272 100644 --- a/filter_linux.go +++ b/filter_linux.go @@ -52,17 +52,17 @@ func FilterAdd(filter Filter) error { } sel.Keys = append(sel.Keys, nl.TcU32Key{}) nl.NewRtAttrChild(options, nl.TCA_U32_SEL, sel.Serialize()) - actions := nl.NewRtAttrChild(options, nl.TCA_U32_ACT, nil) - table := nl.NewRtAttrChild(actions, nl.TCA_ACT_TAB, nil) - nl.NewRtAttrChild(table, nl.TCA_KIND, nl.ZeroTerminated("mirred")) - // redirect to other interface - mir := nl.TcMirred{ - Action: nl.TC_ACT_STOLEN, - Eaction: nl.TCA_EGRESS_REDIR, - Ifindex: uint32(u32.RedirIndex), + if u32.ClassId != 0 { + nl.NewRtAttrChild(options, nl.TCA_U32_CLASSID, nl.Uint32Attr(u32.ClassId)) + } + actionsAttr := nl.NewRtAttrChild(options, nl.TCA_U32_ACT, nil) + // backwards compatibility + if u32.RedirIndex != 0 { + u32.Actions = append([]Action{NewMirredAction(u32.RedirIndex)}, u32.Actions...) + } + if err := encodeActions(actionsAttr, u32.Actions); err != nil { + return err } - aopts := nl.NewRtAttrChild(table, nl.TCA_OPTIONS, nil) - nl.NewRtAttrChild(aopts, nl.TCA_MIRRED_PARMS, mir.Serialize()) } else if fw, ok := filter.(*Fw); ok { if fw.Mask != 0 { b := make([]byte, 4) @@ -151,21 +151,17 @@ func FilterList(link Link, parent uint32) ([]Filter, error) { filter = &GenericFilter{FilterType: filterType} } case nl.TCA_OPTIONS: + data, err := nl.ParseRouteAttr(attr.Value) + if err != nil { + return nil, err + } switch filterType { case "u32": - data, err := nl.ParseRouteAttr(attr.Value) - if err != nil { - return nil, err - } detailed, err = parseU32Data(filter, data) if err != nil { return nil, err } case "fw": - data, err := nl.ParseRouteAttr(attr.Value) - if err != nil { - return nil, err - } detailed, err = parseFwData(filter, data) if err != nil { return nil, err @@ -183,6 +179,85 @@ func FilterList(link Link, parent uint32) ([]Filter, error) { return res, nil } +func encodeActions(attr *nl.RtAttr, actions []Action) error { + tabIndex := int(nl.TCA_ACT_TAB) + + for _, action := range actions { + switch action := action.(type) { + default: + return fmt.Errorf("unknown action type %s", action.Type()) + case *MirredAction: + table := nl.NewRtAttrChild(attr, tabIndex, nil) + tabIndex++ + nl.NewRtAttrChild(table, nl.TCA_ACT_KIND, nl.ZeroTerminated("mirred")) + aopts := nl.NewRtAttrChild(table, nl.TCA_ACT_OPTIONS, nil) + nl.NewRtAttrChild(aopts, nl.TCA_MIRRED_PARMS, action.Serialize()) + case *BpfAction: + table := nl.NewRtAttrChild(attr, tabIndex, nil) + tabIndex++ + nl.NewRtAttrChild(table, nl.TCA_ACT_KIND, nl.ZeroTerminated("bpf")) + aopts := nl.NewRtAttrChild(table, nl.TCA_ACT_OPTIONS, nil) + nl.NewRtAttrChild(aopts, nl.TCA_ACT_BPF_PARMS, action.Serialize()) + nl.NewRtAttrChild(aopts, nl.TCA_ACT_BPF_FD, nl.Uint32Attr(uint32(action.Fd))) + nl.NewRtAttrChild(aopts, nl.TCA_ACT_BPF_NAME, nl.ZeroTerminated(action.Name)) + } + } + return nil +} + +func parseActions(tables []syscall.NetlinkRouteAttr) ([]Action, error) { + var actions []Action + for _, table := range tables { + var action Action + var actionType string + aattrs, err := nl.ParseRouteAttr(table.Value) + if err != nil { + return nil, err + } + nextattr: + for _, aattr := range aattrs { + switch aattr.Attr.Type { + case nl.TCA_KIND: + actionType = string(aattr.Value[:len(aattr.Value)-1]) + // only parse if the action is mirred or bpf + switch actionType { + case "mirred": + action = &MirredAction{} + case "bpf": + action = &BpfAction{} + default: + break nextattr + } + case nl.TCA_OPTIONS: + adata, err := nl.ParseRouteAttr(aattr.Value) + if err != nil { + return nil, err + } + for _, adatum := range adata { + switch actionType { + case "mirred": + switch adatum.Attr.Type { + case nl.TCA_MIRRED_PARMS: + action.(*MirredAction).TcMirred = *nl.DeserializeTcMirred(adatum.Value) + } + case "bpf": + switch adatum.Attr.Type { + case nl.TCA_ACT_BPF_PARMS: + action.(*BpfAction).TcActBpf = *nl.DeserializeTcActBpf(adatum.Value) + case nl.TCA_ACT_BPF_FD: + action.(*BpfAction).Fd = int(native.Uint32(adatum.Value[0:4])) + case nl.TCA_ACT_BPF_NAME: + action.(*BpfAction).Name = string(adatum.Value[:len(adatum.Value)-1]) + } + } + } + } + } + actions = append(actions, action) + } + return actions, nil +} + func parseU32Data(filter Filter, data []syscall.NetlinkRouteAttr) (bool, error) { native = nl.NativeEndian() u32 := filter.(*U32) @@ -197,34 +272,17 @@ func parseU32Data(filter Filter, data []syscall.NetlinkRouteAttr) (bool, error) return detailed, nil } case nl.TCA_U32_ACT: - table, err := nl.ParseRouteAttr(datum.Value) + tables, err := nl.ParseRouteAttr(datum.Value) if err != nil { return detailed, err } - if len(table) != 1 || table[0].Attr.Type != nl.TCA_ACT_TAB { - return detailed, fmt.Errorf("Action table not formed properly") + u32.Actions, err = parseActions(tables) + if err != nil { + return detailed, err } - aattrs, err := nl.ParseRouteAttr(table[0].Value) - for _, aattr := range aattrs { - switch aattr.Attr.Type { - case nl.TCA_KIND: - actionType := string(aattr.Value[:len(aattr.Value)-1]) - // only parse if the action is mirred - if actionType != "mirred" { - return detailed, nil - } - case nl.TCA_OPTIONS: - adata, err := nl.ParseRouteAttr(aattr.Value) - if err != nil { - return detailed, err - } - for _, adatum := range adata { - switch adatum.Attr.Type { - case nl.TCA_MIRRED_PARMS: - mir := nl.DeserializeTcMirred(adatum.Value) - u32.RedirIndex = int(mir.Ifindex) - } - } + for _, action := range u32.Actions { + if action, ok := action.(*MirredAction); ok { + u32.RedirIndex = int(action.Ifindex) } } } diff --git a/filter_test.go b/filter_test.go index 8353d25..70b02fb 100644 --- a/filter_test.go +++ b/filter_test.go @@ -246,3 +246,125 @@ func TestFilterFwAddDel(t *testing.T) { t.Fatal("Failed to remove qdisc") } } + +func TestFilterU32BpfAddDel(t *testing.T) { + tearDown := setUpNetlinkTest(t) + defer tearDown() + if err := LinkAdd(&Ifb{LinkAttrs{Name: "foo"}}); err != nil { + t.Fatal(err) + } + if err := LinkAdd(&Ifb{LinkAttrs{Name: "bar"}}); err != nil { + t.Fatal(err) + } + link, err := LinkByName("foo") + if err != nil { + t.Fatal(err) + } + if err := LinkSetUp(link); err != nil { + t.Fatal(err) + } + redir, err := LinkByName("bar") + if err != nil { + t.Fatal(err) + } + if err := LinkSetUp(redir); err != nil { + t.Fatal(err) + } + qdisc := &Ingress{ + QdiscAttrs: QdiscAttrs{ + LinkIndex: link.Attrs().Index, + Handle: MakeHandle(0xffff, 0), + Parent: HANDLE_INGRESS, + }, + } + if err := QdiscAdd(qdisc); err != nil { + t.Fatal(err) + } + qdiscs, err := QdiscList(link) + if err != nil { + t.Fatal(err) + } + if len(qdiscs) != 1 { + t.Fatal("Failed to add qdisc") + } + _, ok := qdiscs[0].(*Ingress) + if !ok { + t.Fatal("Qdisc is the wrong type") + } + + fd, err := loadSimpleBpf(BPF_PROG_TYPE_SCHED_ACT) + if err != nil { + t.Fatal(err) + } + filter := &U32{ + FilterAttrs: FilterAttrs{ + LinkIndex: link.Attrs().Index, + Parent: MakeHandle(0xffff, 0), + Priority: 1, + Protocol: syscall.ETH_P_ALL, + }, + ClassId: MakeHandle(1, 1), + Actions: []Action{ + &BpfAction{Fd: fd, Name: "simple"}, + &MirredAction{ + TcMirred: nl.TcMirred{ + TcGen: nl.TcGen{Action: nl.TC_ACT_STOLEN}, + Eaction: nl.TCA_EGRESS_REDIR, + Ifindex: uint32(redir.Attrs().Index), + }, + }, + }, + } + + if err := FilterAdd(filter); err != nil { + t.Fatal(err) + } + + filters, err := FilterList(link, MakeHandle(0xffff, 0)) + if err != nil { + t.Fatal(err) + } + if len(filters) != 1 { + t.Fatal("Failed to add filter") + } + u32, ok := filters[0].(*U32) + if !ok { + t.Fatal("Filter is the wrong type") + } + + if len(u32.Actions) != 2 { + t.Fatalf("Too few Actions in filter") + } + bpfAction, ok := u32.Actions[0].(*BpfAction) + if !ok { + t.Fatal("Action[0] is the wrong type") + } + if bpfAction.Fd != fd { + t.Fatal("Action Fd does not match") + } + if _, ok := u32.Actions[1].(*MirredAction); !ok { + t.Fatal("Action[1] is the wrong type") + } + + if err := FilterDel(filter); err != nil { + t.Fatal(err) + } + filters, err = FilterList(link, MakeHandle(0xffff, 0)) + if err != nil { + t.Fatal(err) + } + if len(filters) != 0 { + t.Fatal("Failed to remove filter") + } + + if err := QdiscDel(qdisc); err != nil { + t.Fatal(err) + } + qdiscs, err = QdiscList(link) + if err != nil { + t.Fatal(err) + } + if len(qdiscs) != 0 { + t.Fatal("Failed to remove qdisc") + } +}