Skip to content

Commit

Permalink
Adds ConntrackCreate & ConntrackUpdate
Browse files Browse the repository at this point in the history
 - Also refactored setUpNetlinkTestWithKModule function to reduce redundant NS's created and checks made.

 - Add conntrack protoinfo TCP support + groundwork for other protocols.

 - Tests to cover the above.
  • Loading branch information
aaaaaaaalex authored and aboch committed Jul 4, 2024
1 parent a1c5e02 commit aed23db
Show file tree
Hide file tree
Showing 6 changed files with 1,008 additions and 42 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea/
.vscode/
272 changes: 263 additions & 9 deletions conntrack_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ func ConntrackTableFlush(table ConntrackTableType) error {
return pkgHandle.ConntrackTableFlush(table)
}

// ConntrackCreate creates a new conntrack flow in the desired table
// conntrack -I [table] Create a conntrack or expectation
func ConntrackCreate(table ConntrackTableType, family InetFamily, flow *ConntrackFlow) error {
return pkgHandle.ConntrackCreate(table, family, flow)
}

// ConntrackUpdate updates an existing conntrack flow in the desired table using the handle
// conntrack -U [table] Update a conntrack
func ConntrackUpdate(table ConntrackTableType, family InetFamily, flow *ConntrackFlow) error {
return pkgHandle.ConntrackUpdate(table, family, flow)
}

// 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) {
Expand Down Expand Up @@ -87,6 +99,40 @@ func (h *Handle) ConntrackTableFlush(table ConntrackTableType) error {
return err
}

// ConntrackCreate creates a new conntrack flow in the desired table using the handle
// conntrack -I [table] Create a conntrack or expectation
func (h *Handle) ConntrackCreate(table ConntrackTableType, family InetFamily, flow *ConntrackFlow) error {
req := h.newConntrackRequest(table, family, nl.IPCTNL_MSG_CT_NEW, unix.NLM_F_ACK|unix.NLM_F_CREATE)
attr, err := flow.toNlData()
if err != nil {
return err
}

for _, a := range attr {
req.AddData(a)
}

_, err = req.Execute(unix.NETLINK_NETFILTER, 0)
return err
}

// ConntrackUpdate updates an existing conntrack flow in the desired table using the handle
// conntrack -U [table] Update a conntrack
func (h *Handle) ConntrackUpdate(table ConntrackTableType, family InetFamily, flow *ConntrackFlow) error {
req := h.newConntrackRequest(table, family, nl.IPCTNL_MSG_CT_NEW, unix.NLM_F_ACK|unix.NLM_F_REPLACE)
attr, err := flow.toNlData()
if err != nil {
return err
}

for _, a := range attr {
req.AddData(a)
}

_, err = req.Execute(unix.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) {
Expand Down Expand Up @@ -128,10 +174,44 @@ func (h *Handle) dumpConntrackTable(table ConntrackTableType, family InetFamily)
return req.Execute(unix.NETLINK_NETFILTER, 0)
}

// ProtoInfo wraps an L4-protocol structure - roughly corresponds to the
// __nfct_protoinfo union found in libnetfilter_conntrack/include/internal/object.h.
// Currently, only protocol names, and TCP state is supported.
type ProtoInfo interface {
Protocol() string
}

// ProtoInfoTCP corresponds to the `tcp` struct of the __nfct_protoinfo union.
// Only TCP state is currently supported.
type ProtoInfoTCP struct {
State uint8
}
// Protocol returns "tcp".
func (*ProtoInfoTCP) Protocol() string {return "tcp"}
func (p *ProtoInfoTCP) toNlData() ([]*nl.RtAttr, error) {
ctProtoInfo := nl.NewRtAttr(unix.NLA_F_NESTED | nl.CTA_PROTOINFO, []byte{})
ctProtoInfoTCP := nl.NewRtAttr(unix.NLA_F_NESTED|nl.CTA_PROTOINFO_TCP, []byte{})
ctProtoInfoTCPState := nl.NewRtAttr(nl.CTA_PROTOINFO_TCP_STATE, nl.Uint8Attr(p.State))
ctProtoInfoTCP.AddChild(ctProtoInfoTCPState)
ctProtoInfo.AddChild(ctProtoInfoTCP)

return []*nl.RtAttr{ctProtoInfo}, nil
}

// ProtoInfoSCTP only supports the protocol name.
type ProtoInfoSCTP struct {}
// Protocol returns "sctp".
func (*ProtoInfoSCTP) Protocol() string {return "sctp"}

// ProtoInfoDCCP only supports the protocol name.
type ProtoInfoDCCP struct {}
// Protocol returns "dccp".
func (*ProtoInfoDCCP) Protocol() string {return "dccp"}

// 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 {
type IPTuple struct {
Bytes uint64
DstIP net.IP
DstPort uint16
Expand All @@ -141,16 +221,49 @@ type ipTuple struct {
SrcPort uint16
}

// toNlData generates the inner fields of a nested tuple netlink datastructure
// does not generate the "nested"-flagged outer message.
func (t *IPTuple) toNlData(family uint8) ([]*nl.RtAttr, error) {

var srcIPsFlag, dstIPsFlag int
if family == nl.FAMILY_V4 {
srcIPsFlag = nl.CTA_IP_V4_SRC
dstIPsFlag = nl.CTA_IP_V4_DST
} else if family == nl.FAMILY_V6 {
srcIPsFlag = nl.CTA_IP_V6_SRC
dstIPsFlag = nl.CTA_IP_V6_DST
} else {
return []*nl.RtAttr{}, fmt.Errorf("couldn't generate netlink message for tuple due to unrecognized FamilyType '%d'", family)
}

ctTupleIP := nl.NewRtAttr(unix.NLA_F_NESTED|nl.CTA_TUPLE_IP, nil)
ctTupleIPSrc := nl.NewRtAttr(srcIPsFlag, t.SrcIP)
ctTupleIP.AddChild(ctTupleIPSrc)
ctTupleIPDst := nl.NewRtAttr(dstIPsFlag, t.DstIP)
ctTupleIP.AddChild(ctTupleIPDst)

ctTupleProto := nl.NewRtAttr(unix.NLA_F_NESTED|nl.CTA_TUPLE_PROTO, nil)
ctTupleProtoNum := nl.NewRtAttr(nl.CTA_PROTO_NUM, []byte{t.Protocol})
ctTupleProto.AddChild(ctTupleProtoNum)
ctTupleProtoSrcPort := nl.NewRtAttr(nl.CTA_PROTO_SRC_PORT, nl.BEUint16Attr(t.SrcPort))
ctTupleProto.AddChild(ctTupleProtoSrcPort)
ctTupleProtoDstPort := nl.NewRtAttr(nl.CTA_PROTO_DST_PORT, nl.BEUint16Attr(t.DstPort))
ctTupleProto.AddChild(ctTupleProtoDstPort, )

return []*nl.RtAttr{ctTupleIP, ctTupleProto}, nil
}

type ConntrackFlow struct {
FamilyType uint8
Forward ipTuple
Reverse ipTuple
Forward IPTuple
Reverse IPTuple
Mark uint32
Zone uint16
TimeStart uint64
TimeStop uint64
TimeOut uint32
Labels []byte
ProtoInfo ProtoInfo
}

func (s *ConntrackFlow) String() string {
Expand All @@ -175,14 +288,93 @@ func (s *ConntrackFlow) String() string {
return res
}

// toNlData generates netlink messages representing the flow.
func (s *ConntrackFlow) toNlData() ([]*nl.RtAttr, error) {
var payload []*nl.RtAttr
// The message structure is built as follows:
// <len, NLA_F_NESTED|CTA_TUPLE_ORIG>
// <len, NLA_F_NESTED|CTA_TUPLE_IP>
// <len, [CTA_IP_V4_SRC|CTA_IP_V6_SRC]>
// <IP>
// <len, [CTA_IP_V4_DST|CTA_IP_V6_DST]>
// <IP>
// <len, NLA_F_NESTED|nl.CTA_TUPLE_PROTO>
// <len, CTA_PROTO_NUM>
// <uint8>
// <len, CTA_PROTO_SRC_PORT>
// <BEuint16>
// <len, CTA_PROTO_DST_PORT>
// <BEuint16>
// <len, NLA_F_NESTED|CTA_TUPLE_REPLY>
// <len, NLA_F_NESTED|CTA_TUPLE_IP>
// <len, [CTA_IP_V4_SRC|CTA_IP_V6_SRC]>
// <IP>
// <len, [CTA_IP_V4_DST|CTA_IP_V6_DST]>
// <IP>
// <len, NLA_F_NESTED|nl.CTA_TUPLE_PROTO>
// <len, CTA_PROTO_NUM>
// <uint8>
// <len, CTA_PROTO_SRC_PORT>
// <BEuint16>
// <len, CTA_PROTO_DST_PORT>
// <BEuint16>
// <len, CTA_STATUS>
// <uint64>
// <len, CTA_MARK>
// <BEuint64>
// <len, CTA_TIMEOUT>
// <BEuint64>
// <len, NLA_F_NESTED|CTA_PROTOINFO>

// CTA_TUPLE_ORIG
ctTupleOrig := nl.NewRtAttr(unix.NLA_F_NESTED|nl.CTA_TUPLE_ORIG, nil)
forwardFlowAttrs, err := s.Forward.toNlData(s.FamilyType)
if err != nil {
return nil, fmt.Errorf("couldn't generate netlink data for conntrack forward flow: %w", err)
}
for _, a := range forwardFlowAttrs {
ctTupleOrig.AddChild(a)
}

// CTA_TUPLE_REPLY
ctTupleReply := nl.NewRtAttr(unix.NLA_F_NESTED|nl.CTA_TUPLE_REPLY, nil)
reverseFlowAttrs, err := s.Reverse.toNlData(s.FamilyType)
if err != nil {
return nil, fmt.Errorf("couldn't generate netlink data for conntrack reverse flow: %w", err)
}
for _, a := range reverseFlowAttrs {
ctTupleReply.AddChild(a)
}

ctMark := nl.NewRtAttr(nl.CTA_MARK, nl.BEUint32Attr(s.Mark))
ctTimeout := nl.NewRtAttr(nl.CTA_TIMEOUT, nl.BEUint32Attr(s.TimeOut))

payload = append(payload, ctTupleOrig, ctTupleReply, ctMark, ctTimeout)

if s.ProtoInfo != nil {
switch p := s.ProtoInfo.(type) {
case *ProtoInfoTCP:
attrs, err := p.toNlData()
if err != nil {
return nil, fmt.Errorf("couldn't generate netlink data for conntrack flow's TCP protoinfo: %w", err)
}
payload = append(payload, attrs...)
default:
return nil, errors.New("couldn't generate netlink data for conntrack: field 'ProtoInfo' only supports TCP or nil")
}
}

return payload, nil
}

// 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 {
func parseIpTuple(reader *bytes.Reader, tpl *IPTuple) uint8 {
for i := 0; i < 2; i++ {
_, t, _, v := parseNfAttrTLV(reader)
switch t {
Expand All @@ -201,7 +393,7 @@ func parseIpTuple(reader *bytes.Reader, tpl *ipTuple) uint8 {
tpl.Protocol = uint8(v[0])
}
// We only parse TCP & UDP headers. Skip the others.
if tpl.Protocol != 6 && tpl.Protocol != 17 {
if tpl.Protocol != unix.IPPROTO_TCP && tpl.Protocol != unix.IPPROTO_UDP {
// skip the rest
bytesRemaining := protoInfoTotalLen - protoInfoBytesRead
reader.Seek(int64(bytesRemaining), seekCurrent)
Expand Down Expand Up @@ -250,9 +442,13 @@ func parseNfAttrTL(r *bytes.Reader) (isNested bool, attrType, len uint16) {
return isNested, attrType, len
}

func skipNfAttrValue(r *bytes.Reader, len uint16) {
// skipNfAttrValue seeks `r` past attr of length `len`.
// Maintains buffer alignment.
// Returns length of the seek performed.
func skipNfAttrValue(r *bytes.Reader, len uint16) uint16 {
len = (len + nl.NLA_ALIGNTO - 1) & ^(nl.NLA_ALIGNTO - 1)
r.Seek(int64(len), seekCurrent)
return len
}

func parseBERaw16(r *bytes.Reader, v *uint16) {
Expand All @@ -267,6 +463,10 @@ func parseBERaw64(r *bytes.Reader, v *uint64) {
binary.Read(r, binary.BigEndian, v)
}

func parseRaw32(r *bytes.Reader, v *uint32) {
binary.Read(r, nl.NativeEndian(), v)
}

func parseByteAndPacketCounters(r *bytes.Reader) (bytes, packets uint64) {
for i := 0; i < 2; i++ {
switch _, t, _ := parseNfAttrTL(r); t {
Expand Down Expand Up @@ -306,6 +506,60 @@ func parseTimeStamp(r *bytes.Reader, readSize uint16) (tstart, tstop uint64) {

}

func parseProtoInfoTCPState(r *bytes.Reader) (s uint8) {
binary.Read(r, binary.BigEndian, &s)
r.Seek(nl.SizeofNfattr - 1, seekCurrent)
return s
}

// parseProtoInfoTCP reads the entire nested protoinfo structure, but only parses the state attr.
func parseProtoInfoTCP(r *bytes.Reader, attrLen uint16) (*ProtoInfoTCP) {
p := new(ProtoInfoTCP)
bytesRead := 0
for bytesRead < int(attrLen) {
_, t, l := parseNfAttrTL(r)
bytesRead += nl.SizeofNfattr

switch t {
case nl.CTA_PROTOINFO_TCP_STATE:
p.State = parseProtoInfoTCPState(r)
bytesRead += nl.SizeofNfattr
default:
bytesRead += int(skipNfAttrValue(r, l))
}
}

return p
}

func parseProtoInfo(r *bytes.Reader, attrLen uint16) (p ProtoInfo) {
bytesRead := 0
for bytesRead < int(attrLen) {
_, t, l := parseNfAttrTL(r)
bytesRead += nl.SizeofNfattr

switch t {
case nl.CTA_PROTOINFO_TCP:
p = parseProtoInfoTCP(r, l)
bytesRead += int(l)
// No inner fields of DCCP / SCTP currently supported.
case nl.CTA_PROTOINFO_DCCP:
p = new(ProtoInfoDCCP)
skipped := skipNfAttrValue(r, l)
bytesRead += int(skipped)
case nl.CTA_PROTOINFO_SCTP:
p = new(ProtoInfoSCTP)
skipped := skipNfAttrValue(r, l)
bytesRead += int(skipped)
default:
skipped := skipNfAttrValue(r, l)
bytesRead += int(skipped)
}
}

return p
}

func parseTimeOut(r *bytes.Reader) (ttimeout uint32) {
parseBERaw32(r, &ttimeout)
return
Expand Down Expand Up @@ -365,19 +619,19 @@ func parseRawData(data []byte) *ConntrackFlow {
case nl.CTA_TIMESTAMP:
s.TimeStart, s.TimeStop = parseTimeStamp(reader, l)
case nl.CTA_PROTOINFO:
skipNfAttrValue(reader, l)
s.ProtoInfo = parseProtoInfo(reader, l)
default:
skipNfAttrValue(reader, l)
}
} else {
switch t {
case nl.CTA_MARK:
s.Mark = parseConnectionMark(reader)
case nl.CTA_LABELS:
case nl.CTA_LABELS:
s.Labels = parseConnectionLabels(reader)
case nl.CTA_TIMEOUT:
s.TimeOut = parseTimeOut(reader)
case nl.CTA_STATUS, nl.CTA_USE, nl.CTA_ID:
case nl.CTA_ID, nl.CTA_STATUS, nl.CTA_USE:
skipNfAttrValue(reader, l)
case nl.CTA_ZONE:
s.Zone = parseConnectionZone(reader)
Expand Down
Loading

0 comments on commit aed23db

Please sign in to comment.