From 49a0ee6ac765c371618e1f9c6bd347e1556d3e92 Mon Sep 17 00:00:00 2001 From: Antonio Ojea Date: Fri, 5 Jun 2020 01:38:34 +0200 Subject: [PATCH] conntrack filter by port and protocol Add a new method to the ConntrackFilter to be able to filter conntrack entries by Layer 4 protocol and source and destination port. Signed-off-by: Antonio Ojea --- conntrack_linux.go | 118 +++++++++++++++++++++++++-------- conntrack_test.go | 159 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 217 insertions(+), 60 deletions(-) diff --git a/conntrack_linux.go b/conntrack_linux.go index 4bff0dcba..4ec90c47e 100644 --- a/conntrack_linux.go +++ b/conntrack_linux.go @@ -318,18 +318,25 @@ func parseRawData(data []byte) *ConntrackFlow { // --mask-src ip Source mask address // --mask-dst ip Destination mask address +// Layer 4 Protocol common parameters and options: +// TCP, UDP, SCTP, UDPLite and DCCP +// --sport, --orig-port-src port Source port in original direction +// --dport, --orig-port-dst port Destination port in original direction + // Filter types type ConntrackFilterType uint8 const ( - ConntrackOrigSrcIP = iota // -orig-src ip Source address from original direction - ConntrackOrigDstIP // -orig-dst ip Destination address from original direction - ConntrackReplySrcIP // --reply-src ip Reply Source IP - ConntrackReplyDstIP // --reply-dst ip Reply Destination IP - ConntrackReplyAnyIP // Match source or destination reply IP - ConntrackNatSrcIP = ConntrackReplySrcIP // deprecated use instead ConntrackReplySrcIP - ConntrackNatDstIP = ConntrackReplyDstIP // deprecated use instead ConntrackReplyDstIP - ConntrackNatAnyIP = ConntrackReplyAnyIP // deprecated use instaed ConntrackReplyAnyIP + ConntrackOrigSrcIP = iota // -orig-src ip Source address from original direction + ConntrackOrigDstIP // -orig-dst ip Destination address from original direction + ConntrackReplySrcIP // --reply-src ip Reply Source IP + ConntrackReplyDstIP // --reply-dst ip Reply Destination IP + ConntrackReplyAnyIP // Match source or destination reply IP + ConntrackOrigSrcPort // --orig-port-src port Source port in original direction + ConntrackOrigDstPort // --orig-port-dst port Destination port in original direction + ConntrackNatSrcIP = ConntrackReplySrcIP // deprecated use instead ConntrackReplySrcIP + ConntrackNatDstIP = ConntrackReplyDstIP // deprecated use instead ConntrackReplyDstIP + ConntrackNatAnyIP = ConntrackReplyAnyIP // deprecated use instead ConntrackReplyAnyIP ) type CustomConntrackFilter interface { @@ -339,7 +346,9 @@ type CustomConntrackFilter interface { } type ConntrackFilter struct { - ipFilter map[ConntrackFilterType]net.IP + ipFilter map[ConntrackFilterType]net.IP + portFilter map[ConntrackFilterType]uint16 + protoFilter uint8 } // AddIP adds an IP to the conntrack filter @@ -354,38 +363,91 @@ func (f *ConntrackFilter) AddIP(tp ConntrackFilterType, ip net.IP) error { return nil } +// AddPort adds a Port to the conntrack filter if the Layer 4 protocol allows it +func (f *ConntrackFilter) AddPort(tp ConntrackFilterType, port uint16) error { + switch f.protoFilter { + // TCP, UDP, DCCP, SCTP, UDPLite + case 6, 17, 33, 132, 136: + default: + return errors.New("Filter attribute not available without a Layer 4 protocol") + } + + if f.portFilter == nil { + f.portFilter = make(map[ConntrackFilterType]uint16) + } + if _, ok := f.portFilter[tp]; ok { + return errors.New("Filter attribute already present") + } + f.portFilter[tp] = port + return nil +} + +// AddProtocol adds the Layer 4 protocol to the conntrack filter +func (f *ConntrackFilter) AddProtocol(proto uint8) error { + if f.protoFilter != 0 { + return errors.New("Filter attribute already present") + } + f.protoFilter = proto + return nil +} + // MatchConntrackFlow applies the filter to the flow and returns true if the flow matches the filter // false otherwise func (f *ConntrackFilter) MatchConntrackFlow(flow *ConntrackFlow) bool { - if len(f.ipFilter) == 0 { + if len(f.ipFilter) == 0 && len(f.portFilter) == 0 && f.protoFilter == 0 { // empty filter always not match return false } - match := true - // -orig-src ip Source address from original direction - if elem, found := f.ipFilter[ConntrackOrigSrcIP]; found { - match = match && elem.Equal(flow.Forward.SrcIP) + // -p, --protonum proto Layer 4 Protocol, eg. 'tcp' + if f.protoFilter != 0 && flow.Forward.Protocol != f.protoFilter { + // different Layer 4 protocol always not match + return false } - // -orig-dst ip Destination address from original direction - if elem, found := f.ipFilter[ConntrackOrigDstIP]; match && found { - match = match && elem.Equal(flow.Forward.DstIP) - } + match := true - // -src-nat ip Source NAT ip - if elem, found := f.ipFilter[ConntrackReplySrcIP]; match && found { - match = match && elem.Equal(flow.Reverse.SrcIP) - } + // IP conntrack filter + if len(f.ipFilter) > 0 { + + // -orig-src ip Source address from original direction + if elem, found := f.ipFilter[ConntrackOrigSrcIP]; found { + match = match && elem.Equal(flow.Forward.SrcIP) + } + + // -orig-dst ip Destination address from original direction + if elem, found := f.ipFilter[ConntrackOrigDstIP]; match && found { + match = match && elem.Equal(flow.Forward.DstIP) + } + + // -src-nat ip Source NAT ip + if elem, found := f.ipFilter[ConntrackReplySrcIP]; match && found { + match = match && elem.Equal(flow.Reverse.SrcIP) + } + + // -dst-nat ip Destination NAT ip + if elem, found := f.ipFilter[ConntrackReplyDstIP]; match && found { + match = match && elem.Equal(flow.Reverse.DstIP) + } - // -dst-nat ip Destination NAT ip - if elem, found := f.ipFilter[ConntrackReplyDstIP]; match && found { - match = match && elem.Equal(flow.Reverse.DstIP) + // Match source or destination reply IP + if elem, found := f.ipFilter[ConntrackReplyAnyIP]; match && found { + match = match && (elem.Equal(flow.Reverse.SrcIP) || elem.Equal(flow.Reverse.DstIP)) + } } - // Match source or destination reply IP - if elem, found := f.ipFilter[ConntrackReplyAnyIP]; match && found { - match = match && (elem.Equal(flow.Reverse.SrcIP) || elem.Equal(flow.Reverse.DstIP)) + // Layer 4 Port filter + if len(f.portFilter) > 0 { + + // -orig-port-src port Source port from original direction + if elem, found := f.portFilter[ConntrackOrigSrcPort]; match && found { + match = match && elem == flow.Forward.SrcPort + } + + // -orig-port-dst port Destination port from original direction + if elem, found := f.portFilter[ConntrackOrigDstPort]; match && found { + match = match && elem == flow.Forward.DstPort + } } return match diff --git a/conntrack_test.go b/conntrack_test.go index edc9f5be5..83932e14d 100644 --- a/conntrack_test.go +++ b/conntrack_test.go @@ -92,10 +92,18 @@ func TestConntrackSocket(t *testing.T) { // Creates some flows and checks that they are correctly fetched from the conntrack table func TestConntrackTableList(t *testing.T) { skipUnlessRoot(t) + k, m, err := KernelVersion() + if err != nil { + t.Fatal(err) + } + // conntrack l3proto was unified since 4.19 + // https://github.com/torvalds/linux/commit/a0ae2562c6c4b2721d9fddba63b7286c13517d9f + if k < 4 || k == 4 && m < 19 { + setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4") + setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv6") + } setUpNetlinkTestWithKModule(t, "nf_conntrack") setUpNetlinkTestWithKModule(t, "nf_conntrack_netlink") - setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4") - setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv6") // Creates a new namespace and bring up the loopback interface origns, ns, h := nsCreateAndEnter(t) @@ -107,7 +115,7 @@ func TestConntrackTableList(t *testing.T) { setUpF(t, "/proc/sys/net/netfilter/nf_conntrack_acct", "1") // Flush the table to start fresh - err := h.ConntrackTableFlush(ConntrackTable) + err = h.ConntrackTableFlush(ConntrackTable) CheckErrorFail(t, err) // Create 5 udp @@ -149,8 +157,16 @@ func TestConntrackTableFlush(t *testing.T) { skipUnlessRoot(t) setUpNetlinkTestWithKModule(t, "nf_conntrack") setUpNetlinkTestWithKModule(t, "nf_conntrack_netlink") - setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4") - + k, m, err := KernelVersion() + if err != nil { + t.Fatal(err) + } + // conntrack l3proto was unified since 4.19 + // https://github.com/torvalds/linux/commit/a0ae2562c6c4b2721d9fddba63b7286c13517d9f + if k < 4 || k == 4 && m < 19 { + setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4") + } + setUpNetlinkTestWithKModule(t, "nf_conntrack") // Creates a new namespace and bring up the loopback interface origns, ns, h := nsCreateAndEnter(t) defer netns.Set(*origns) @@ -211,7 +227,15 @@ func TestConntrackTableDelete(t *testing.T) { skipUnlessRoot(t) setUpNetlinkTestWithKModule(t, "nf_conntrack") setUpNetlinkTestWithKModule(t, "nf_conntrack_netlink") - setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4") + k, m, err := KernelVersion() + if err != nil { + t.Fatal(err) + } + // conntrack l3proto was unified since 4.19 + // https://github.com/torvalds/linux/commit/a0ae2562c6c4b2721d9fddba63b7286c13517d9f + if k < 4 || k == 4 && m < 19 { + setUpNetlinkTestWithKModule(t, "nf_conntrack_ipv4") + } // Creates a new namespace and bring up the loopback interface origns, ns, h := nsCreateAndEnter(t) @@ -252,6 +276,8 @@ func TestConntrackTableDelete(t *testing.T) { // Create a filter to erase groupB flows filter := &ConntrackFilter{} filter.AddIP(ConntrackOrigDstIP, net.ParseIP("127.0.0.20")) + filter.AddProtocol(17) + filter.AddPort(ConntrackOrigDstPort, 8000) // Flush entries of groupB var deleted uint @@ -296,46 +322,52 @@ func TestConntrackFilter(t *testing.T) { flowList = append(flowList, ConntrackFlow{ FamilyType: unix.AF_INET, Forward: ipTuple{ - SrcIP: net.ParseIP("10.0.0.1"), - DstIP: net.ParseIP("20.0.0.1"), - SrcPort: 1000, - DstPort: 2000, + SrcIP: net.ParseIP("10.0.0.1"), + DstIP: net.ParseIP("20.0.0.1"), + SrcPort: 1000, + DstPort: 2000, + Protocol: 17, }, Reverse: ipTuple{ - SrcIP: net.ParseIP("20.0.0.1"), - DstIP: net.ParseIP("192.168.1.1"), - SrcPort: 2000, - DstPort: 1000, + SrcIP: net.ParseIP("20.0.0.1"), + DstIP: net.ParseIP("192.168.1.1"), + SrcPort: 2000, + DstPort: 1000, + Protocol: 17, }, }, ConntrackFlow{ FamilyType: unix.AF_INET, Forward: ipTuple{ - SrcIP: net.ParseIP("10.0.0.2"), - DstIP: net.ParseIP("20.0.0.2"), - SrcPort: 5000, - DstPort: 6000, + SrcIP: net.ParseIP("10.0.0.2"), + DstIP: net.ParseIP("20.0.0.2"), + SrcPort: 5000, + DstPort: 6000, + Protocol: 6, }, Reverse: ipTuple{ - SrcIP: net.ParseIP("20.0.0.2"), - DstIP: net.ParseIP("192.168.1.1"), - SrcPort: 6000, - DstPort: 5000, + SrcIP: net.ParseIP("20.0.0.2"), + DstIP: net.ParseIP("192.168.1.1"), + SrcPort: 6000, + DstPort: 5000, + Protocol: 6, }, }, ConntrackFlow{ FamilyType: unix.AF_INET6, Forward: ipTuple{ - SrcIP: net.ParseIP("eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee"), - DstIP: net.ParseIP("dddd:dddd:dddd:dddd:dddd:dddd:dddd:dddd"), - SrcPort: 1000, - DstPort: 2000, + SrcIP: net.ParseIP("eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee"), + DstIP: net.ParseIP("dddd:dddd:dddd:dddd:dddd:dddd:dddd:dddd"), + SrcPort: 1000, + DstPort: 2000, + Protocol: 132, }, Reverse: ipTuple{ - SrcIP: net.ParseIP("dddd:dddd:dddd:dddd:dddd:dddd:dddd:dddd"), - DstIP: net.ParseIP("eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee"), - SrcPort: 2000, - DstPort: 1000, + SrcIP: net.ParseIP("dddd:dddd:dddd:dddd:dddd:dddd:dddd:dddd"), + DstIP: net.ParseIP("eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee"), + SrcPort: 2000, + DstPort: 1000, + Protocol: 132, }, }) @@ -345,11 +377,46 @@ func TestConntrackFilter(t *testing.T) { t.Fatalf("Error, empty filter cannot match, v4:%d, v6:%d", v4Match, v6Match) } - // SrcIP filter + // Filter errors + + // Adding same attribute should fail + filter := &ConntrackFilter{} + filter.AddIP(ConntrackOrigSrcIP, net.ParseIP("10.0.0.1")) + if err := filter.AddIP(ConntrackOrigSrcIP, net.ParseIP("10.0.0.1")); err == nil { + t.Fatalf("Error, it should fail adding same attribute to the filter") + } + filter.AddProtocol(6) + if err := filter.AddProtocol(17); err == nil { + t.Fatalf("Error, it should fail adding same attribute to the filter") + } + filter.AddPort(80) + if err := filter.AddPort(80); err == nil { + t.Fatalf("Error, it should fail adding same attribute to the filter") + } + + // Can not add a Port filter without Layer 4 protocol + filter := &ConntrackFilter{} + if err := filter.AddPort(80); err == nil { + t.Fatalf("Error, it should fail adding a port filter without a protocol") + } + + // Proto filter filterV4 := &ConntrackFilter{} - filterV4.AddIP(ConntrackOrigSrcIP, net.ParseIP("10.0.0.1")) + filterV4.AddProtocol(6) filterV6 := &ConntrackFilter{} + filterV6.AddProtocol(17) + + v4Match, v6Match = applyFilter(flowList, filterV4, filterV6) + if v4Match != 1 || v6Match != 1 { + t.Fatalf("Error, there should be only 1 match for TCP:%d, UDP:%d", v4Match, v6Match) + } + + // SrcIP filter + filterV4 = &ConntrackFilter{} + filterV4.AddIP(ConntrackOrigSrcIP, net.ParseIP("10.0.0.1")) + + filterV6 = &ConntrackFilter{} filterV6.AddIP(ConntrackOrigSrcIP, net.ParseIP("eeee:eeee:eeee:eeee:eeee:eeee:eeee:eeee")) v4Match, v6Match = applyFilter(flowList, filterV4, filterV6) @@ -404,4 +471,32 @@ func TestConntrackFilter(t *testing.T) { if v4Match != 2 || v6Match != 1 { t.Fatalf("Error, there should be an exact match, v4:%d, v6:%d", v4Match, v6Match) } + + // SrcPort filter + filterV4 = &ConntrackFilter{} + filterV4.AddProtocol(6) + filterV4.AddPort(ConntrackOrigSrcPort, 5000) + + filterV6 = &ConntrackFilter{} + filterV6.AddProtocol(132) + filterV6.AddPort(ConntrackOrigSrcPort, 1000) + + v4Match, v6Match = applyFilter(flowList, filterV4, filterV6) + if v4Match != 1 || v6Match != 1 { + t.Fatalf("Error, there should be only 1 match, v4:%d, v6:%d", v4Match, v6Match) + } + + // DstPort filter + filterV4 = &ConntrackFilter{} + filterV4.AddProtocol(6) + filterV4.AddPort(ConntrackOrigDstPort, 6000) + + filterV6 = &ConntrackFilter{} + filterV6.AddProtocol(132) + filterV6.AddPort(ConntrackOrigDstPort, 2000) + + v4Match, v6Match = applyFilter(flowList, filterV4, filterV6) + if v4Match != 1 || v6Match != 1 { + t.Fatalf("Error, there should be only 1 match, v4:%d, v6:%d", v4Match, v6Match) + } }