package netlink import ( "bytes" "encoding/binary" "errors" "fmt" "net" "syscall" "github.com/vishvananda/netlink/nl" ) // ConntrackTableType Conntrack table for the netlink operation type ConntrackTableType uint8 const ( // ConntrackTable Conntrack table // https://github.com/torvalds/linux/blob/master/include/uapi/linux/netfilter/nfnetlink.h -> #define NFNL_SUBSYS_CTNETLINK 1 ConntrackTable = 1 // ConntrackExpectTable Conntrack expect table // https://github.com/torvalds/linux/blob/master/include/uapi/linux/netfilter/nfnetlink.h -> #define NFNL_SUBSYS_CTNETLINK_EXP 2 ConntrackExpectTable = 2 ) const ( // For Parsing Mark TCP_PROTO = 6 UDP_PROTO = 17 ) const ( // backward compatibility with golang 1.6 which does not have io.SeekCurrent seekCurrent = 1 ) // InetFamily Family type type InetFamily uint8 // -L [table] [options] List conntrack or expectation table // -G [table] parameters Get conntrack or expectation // -I [table] parameters Create a conntrack or expectation // -U [table] parameters Update a conntrack // -E [table] [options] Show events // -C [table] Show counter // -S Show statistics // ConntrackTableList returns the flow list of a table of a specific family // conntrack -L [table] [options] List conntrack or expectation table func ConntrackTableList(table ConntrackTableType, family InetFamily) ([]*ConntrackFlow, error) { return pkgHandle.ConntrackTableList(table, family) } // ConntrackTableFlush flushes all the flows of a specified table // conntrack -F [table] Flush table // The flush operation applies to all the family types func ConntrackTableFlush(table ConntrackTableType) error { return pkgHandle.ConntrackTableFlush(table) } // ConntrackDeleteFilter deletes entries on the specified table on the base of the filter // conntrack -D [table] parameters Delete conntrack or expectation func ConntrackDeleteFilter(table ConntrackTableType, family InetFamily, filter CustomConntrackFilter) (uint, error) { return pkgHandle.ConntrackDeleteFilter(table, family, filter) } // ConntrackTableList returns the flow list of a table of a specific family using the netlink handle passed // conntrack -L [table] [options] List conntrack or expectation table func (h *Handle) ConntrackTableList(table ConntrackTableType, family InetFamily) ([]*ConntrackFlow, error) { res, err := h.dumpConntrackTable(table, family) if err != nil { return nil, err } // Deserialize all the flows var result []*ConntrackFlow for _, dataRaw := range res { result = append(result, parseRawData(dataRaw)) } return result, nil } // ConntrackTableFlush flushes all the flows of a specified table using the netlink handle passed // conntrack -F [table] Flush table // The flush operation applies to all the family types func (h *Handle) ConntrackTableFlush(table ConntrackTableType) error { req := h.newConntrackRequest(table, syscall.AF_INET, nl.IPCTNL_MSG_CT_DELETE, syscall.NLM_F_ACK) _, err := req.Execute(syscall.NETLINK_NETFILTER, 0) return err } // ConntrackDeleteFilter deletes entries on the specified table on the base of the filter using the netlink handle passed // conntrack -D [table] parameters Delete conntrack or expectation func (h *Handle) ConntrackDeleteFilter(table ConntrackTableType, family InetFamily, filter CustomConntrackFilter) (uint, error) { res, err := h.dumpConntrackTable(table, family) if err != nil { return 0, err } var matched uint for _, dataRaw := range res { flow := parseRawData(dataRaw) if match := filter.MatchConntrackFlow(flow); match { req2 := h.newConntrackRequest(table, family, nl.IPCTNL_MSG_CT_DELETE, syscall.NLM_F_ACK) // skip the first 4 byte that are the netfilter header, the newConntrackRequest is adding it already req2.AddRawData(dataRaw[4:]) req2.Execute(syscall.NETLINK_NETFILTER, 0) matched++ } } return matched, nil } func (h *Handle) newConntrackRequest(table ConntrackTableType, family InetFamily, operation, flags int) *nl.NetlinkRequest { // Create the Netlink request object req := h.newNetlinkRequest((int(table)<<8)|operation, flags) // Add the netfilter header msg := &nl.Nfgenmsg{ NfgenFamily: uint8(family), Version: nl.NFNETLINK_V0, ResId: 0, } req.AddData(msg) return req } func (h *Handle) dumpConntrackTable(table ConntrackTableType, family InetFamily) ([][]byte, error) { req := h.newConntrackRequest(table, family, nl.IPCTNL_MSG_CT_GET, syscall.NLM_F_DUMP) return req.Execute(syscall.NETLINK_NETFILTER, 0) } // The full conntrack flow structure is very complicated and can be found in the file: // http://git.netfilter.org/libnetfilter_conntrack/tree/include/internal/object.h // For the time being, the structure below allows to parse and extract the base information of a flow type ipTuple struct { SrcIP net.IP DstIP net.IP Protocol uint8 SrcPort uint16 DstPort uint16 } type ConntrackFlow struct { FamilyType uint8 Forward ipTuple Reverse ipTuple Mark uint32 } func (s *ConntrackFlow) String() string { // conntrack cmd output: // udp 17 src=127.0.0.1 dst=127.0.0.1 sport=4001 dport=1234 [UNREPLIED] src=127.0.0.1 dst=127.0.0.1 sport=1234 dport=4001 mark=0 return fmt.Sprintf("%s\t%d src=%s dst=%s sport=%d dport=%d\tsrc=%s dst=%s sport=%d dport=%d mark=%d", nl.L4ProtoMap[s.Forward.Protocol], s.Forward.Protocol, s.Forward.SrcIP.String(), s.Forward.DstIP.String(), s.Forward.SrcPort, s.Forward.DstPort, s.Reverse.SrcIP.String(), s.Reverse.DstIP.String(), s.Reverse.SrcPort, s.Reverse.DstPort, s.Mark) } // This method parse the ip tuple structure // The message structure is the following: // <len, [CTA_IP_V4_SRC|CTA_IP_V6_SRC], 16 bytes for the IP> // <len, [CTA_IP_V4_DST|CTA_IP_V6_DST], 16 bytes for the IP> // <len, NLA_F_NESTED|nl.CTA_TUPLE_PROTO, 1 byte for the protocol, 3 bytes of padding> // <len, CTA_PROTO_SRC_PORT, 2 bytes for the source port, 2 bytes of padding> // <len, CTA_PROTO_DST_PORT, 2 bytes for the source port, 2 bytes of padding> func parseIpTuple(reader *bytes.Reader, tpl *ipTuple) uint8 { for i := 0; i < 2; i++ { _, t, _, v := parseNfAttrTLV(reader) switch t { case nl.CTA_IP_V4_SRC, nl.CTA_IP_V6_SRC: tpl.SrcIP = v case nl.CTA_IP_V4_DST, nl.CTA_IP_V6_DST: tpl.DstIP = v } } // Skip the next 4 bytes nl.NLA_F_NESTED|nl.CTA_TUPLE_PROTO reader.Seek(4, seekCurrent) _, t, _, v := parseNfAttrTLV(reader) if t == nl.CTA_PROTO_NUM { tpl.Protocol = uint8(v[0]) } // Skip some padding 3 bytes reader.Seek(3, seekCurrent) for i := 0; i < 2; i++ { _, t, _ := parseNfAttrTL(reader) switch t { case nl.CTA_PROTO_SRC_PORT: parseBERaw16(reader, &tpl.SrcPort) case nl.CTA_PROTO_DST_PORT: parseBERaw16(reader, &tpl.DstPort) } // Skip some padding 2 byte reader.Seek(2, seekCurrent) } return tpl.Protocol } func parseNfAttrTLV(r *bytes.Reader) (isNested bool, attrType, len uint16, value []byte) { isNested, attrType, len = parseNfAttrTL(r) value = make([]byte, len) binary.Read(r, binary.BigEndian, &value) return isNested, attrType, len, value } func parseNfAttrTL(r *bytes.Reader) (isNested bool, attrType, len uint16) { binary.Read(r, nl.NativeEndian(), &len) len -= nl.SizeofNfattr binary.Read(r, nl.NativeEndian(), &attrType) isNested = (attrType & nl.NLA_F_NESTED) == nl.NLA_F_NESTED attrType = attrType & (nl.NLA_F_NESTED - 1) return isNested, attrType, len } func parseBERaw16(r *bytes.Reader, v *uint16) { binary.Read(r, binary.BigEndian, v) } func parseRawData(data []byte) *ConntrackFlow { s := &ConntrackFlow{} var proto uint8 // First there is the Nfgenmsg header // consume only the family field reader := bytes.NewReader(data) binary.Read(reader, nl.NativeEndian(), &s.FamilyType) // skip rest of the Netfilter header reader.Seek(3, seekCurrent) // The message structure is the following: // <len, NLA_F_NESTED|CTA_TUPLE_ORIG> 4 bytes // <len, NLA_F_NESTED|CTA_TUPLE_IP> 4 bytes // flow information of the forward flow // <len, NLA_F_NESTED|CTA_TUPLE_REPLY> 4 bytes // <len, NLA_F_NESTED|CTA_TUPLE_IP> 4 bytes // flow information of the reverse flow for reader.Len() > 0 { nested, t, l := parseNfAttrTL(reader) if nested && t == nl.CTA_TUPLE_ORIG { if nested, t, _ = parseNfAttrTL(reader); nested && t == nl.CTA_TUPLE_IP { proto = parseIpTuple(reader, &s.Forward) } } else if nested && t == nl.CTA_TUPLE_REPLY { if nested, t, _ = parseNfAttrTL(reader); nested && t == nl.CTA_TUPLE_IP { parseIpTuple(reader, &s.Reverse) // Got all the useful information stop parsing break } else { // Header not recognized skip it reader.Seek(int64(l), seekCurrent) } } } if proto == TCP_PROTO { reader.Seek(64, seekCurrent) _, t, _, v := parseNfAttrTLV(reader) if t == nl.CTA_MARK { s.Mark = uint32(v[3]) } } else if proto == UDP_PROTO { reader.Seek(16, seekCurrent) _, t, _, v := parseNfAttrTLV(reader) if t == nl.CTA_MARK { s.Mark = uint32(v[3]) } } return s } // Conntrack parameters and options: // -n, --src-nat ip source NAT ip // -g, --dst-nat ip destination NAT ip // -j, --any-nat ip source or destination NAT ip // -m, --mark mark Set mark // -c, --secmark secmark Set selinux secmark // -e, --event-mask eventmask Event mask, eg. NEW,DESTROY // -z, --zero Zero counters while listing // -o, --output type[,...] Output format, eg. xml // -l, --label label[,...] conntrack labels // Common parameters and options: // -s, --src, --orig-src ip Source address from original direction // -d, --dst, --orig-dst ip Destination address from original direction // -r, --reply-src ip Source addres from reply direction // -q, --reply-dst ip Destination address from reply direction // -p, --protonum proto Layer 4 Protocol, eg. 'tcp' // -f, --family proto Layer 3 Protocol, eg. 'ipv6' // -t, --timeout timeout Set timeout // -u, --status status Set status, eg. ASSURED // -w, --zone value Set conntrack zone // --orig-zone value Set zone for original direction // --reply-zone value Set zone for reply direction // -b, --buffer-size Netlink socket buffer size // --mask-src ip Source mask address // --mask-dst ip Destination mask address // 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 ConntrackNatSrcIP // -src-nat ip Source NAT ip ConntrackNatDstIP // -dst-nat ip Destination NAT ip ConntrackNatAnyIP // -any-nat ip Source or destination NAT ip ) type CustomConntrackFilter interface { // MatchConntrackFlow applies the filter to the flow and returns true if the flow matches // the filter or false otherwise MatchConntrackFlow(flow *ConntrackFlow) bool } type ConntrackFilter struct { ipFilter map[ConntrackFilterType]net.IP } // AddIP adds an IP to the conntrack filter func (f *ConntrackFilter) AddIP(tp ConntrackFilterType, ip net.IP) error { if f.ipFilter == nil { f.ipFilter = make(map[ConntrackFilterType]net.IP) } if _, ok := f.ipFilter[tp]; ok { return errors.New("Filter attribute already present") } f.ipFilter[tp] = ip 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 { // 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) } // -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[ConntrackNatSrcIP]; match && found { match = match && elem.Equal(flow.Reverse.SrcIP) } // -dst-nat ip Destination NAT ip if elem, found := f.ipFilter[ConntrackNatDstIP]; match && found { match = match && elem.Equal(flow.Reverse.DstIP) } // -any-nat ip Source or destination NAT ip if elem, found := f.ipFilter[ConntrackNatAnyIP]; match && found { match = match && (elem.Equal(flow.Reverse.SrcIP) || elem.Equal(flow.Reverse.DstIP)) } return match } var _ CustomConntrackFilter = (*ConntrackFilter)(nil)