17

conntrack中的数据结构详解

 3 years ago
source link: http://blog.spoock.com/2020/05/16/conntrack-data-structure/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

用户态conntrack原理分析 中也说到过,对数据结构没有进行详细的分析,本篇文章就是深入到conntrack的消息,一步步解析出IP地址和端口号等等信息.

还是以 conntrack-gofirst commit 为例来分析.

分析

由于是对conntrack的信息进行分析, 因此分析对象就是netfilter的返回包信息.所以我们从 parseRawData 来分析

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)
	......

data是一个byte数组,通过bytes.NewReader()方法,得到一个Reader对象.

type Reader struct {
	s        []byte
	i        int64 // current reading index
	prevRune int   // index of previous rune; or < 0
}

当执行完毕 reader := bytes.NewReader(data) 之后,此时的data和reader的内容分别如下:

接下来的分析都是基于这个数据,牢记这个数据.

NjUJBrb.png!web

reader中的s属性和data内容完全一样,是uint8(byte类型)的数组,len为252,cap为65520. 此时的i(即index)为0,表示没有任何的读取(因为Index为0,说明读取指针没有移动).

READ

接下来程序执行 binary.Read(reader, NativeEndian(), &s.FamilyType) , FamilyType 是一个uint8类型的数据.即取第一个元素.前面已经列出了data的数据. [2 0 0 0 52 0 1 128 20 0 1 12.....]

第一个元素是2,对应的是 s.FamilyType 是2. 由于已经读取了一位,所以reader中的index应该为1.

JVNJbym.png!web

数据的结果也和我们分析情况一致.

SEEK

接下来,程序执行如下代码:

const (
	// backward compatibility with golang 1.6 which does not have io.SeekCurrent
	seekCurrent = 1
)
// skip rest of the Netfilter header
reader.Seek(3, seekCurrent)

即,移动4个单位.移动的原因在 用户态conntrack原理分析 中也说到过 是因为在Netlink Data中的前面4个字节一般都是代表 nfgenmsg 信息. 观察此时的reader的信息. 由于reader调用了Seek()移动了3,所以此时的reader的index为4.

QVn2Iny.png!web

parseNfAttrTL

此时reader的index喂,接下来就是执行 parseNfAttrTL(reader) . 此时的reader情况如下所示:

aU3YfaN.png!web

同样一步步来分析整个过程.

func parseNfAttrTL(r *bytes.Reader) (isNested bool, attrType, len uint16) {
 	binary.Read(r, NativeEndian(), &len)
	len -= SizeofNfattr

	binary.Read(r, NativeEndian(), &attrType)
	isNested = (attrType & NLA_F_NESTED) == NLA_F_NESTED
	attrType = attrType & (NLA_F_NESTED - 1)

	return isNested, attrType, len
}

binary.Read(r, NativeEndian(), &len) 由于len是uint16类型,而Reader中的每个元素是uint8,所以len会读取2个元组,即52,0,所以len的长度是52.reader的i为6.如下所示:

UryiE33.png!web

此时reader的状态变为:

fmMJvii.png!web

程序继续执行, binary.Read(r, NativeEndian(), &attrType) .同样由于attrType是uint16,所以读取的数据是1和128.将两者组合成为uint16的数据. 同时 NativeEndian() 返回的小端.所以1和128的组合方式是:

128转换为二进制是:10000000 , 由于是unit8,转换为uint16,需要填充,最终是 10000000 00000000,1的二进制是 00000000 00000001 ,所以两者的组合就是 10000000 00000000 + 00000000 00000001 = 1000000000000001 ,最终转换为十进制就是32769.同时reader的index也会移动2位,变为8. 实际分析结果与数据一致.

fmIraie.png!web

parseNfAttrTL

程序第一次执行完 parseNfAttrTL ,根据attrType确定是 CTA_TUPLE_ORIG ,会继续执行 parseNfAttrTL

if nested, t, l := parseNfAttrTL(reader); nested {
    switch t {
    case CTA_TUPLE_ORIG:
        if nested, t, _ = parseNfAttrTL(reader); nested && t == CTA_TUPLE_IP {
            proto = parseIpTuple(reader, &s.Forward)
        }
        
...................................
func parseNfAttrTL(r *bytes.Reader) (isNested bool, attrType, len uint16) {
 	binary.Read(r, NativeEndian(), &len)
	len -= SizeofNfattr

	binary.Read(r, NativeEndian(), &attrType)
	isNested = (attrType & NLA_F_NESTED) == NLA_F_NESTED
	attrType = attrType & (NLA_F_NESTED - 1)

	return isNested, attrType, len
}

此时的reader的index还是为8.data的读取状态是:

6JJjmam.png!web

同样按照上面的分析方法,len长度变为了16.attrType的值是32769.reader移动4为,所以reader的index值是12.执行完毕之后,最终的data的数据情况如下所示:

vIjyqqM.png!web

parseIpTuple

程序接下来就是执行 parseIpTuple(reader, &s.Forward)

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, NativeEndian(), &len)
	len -= SizeofNfattr

	binary.Read(r, NativeEndian(), &attrType)
	isNested = (attrType & NLA_F_NESTED) == NLA_F_NESTED
	attrType = attrType & (NLA_F_NESTED - 1)

	return isNested, attrType, len
}

// 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 CTA_IP_V4_SRC, CTA_IP_V6_SRC:
			tpl.SrcIP = v
		case CTA_IP_V4_DST, 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 == 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 CTA_PROTO_SRC_PORT:
			parseBERaw16(reader, &tpl.SrcPort)
		case CTA_PROTO_DST_PORT:
			parseBERaw16(reader, &tpl.DstPort)
		}
		// Skip some padding 2 byte
		reader.Seek(2, seekCurrent)
	}
	return tpl.Protocol
}

程序首先会调用 parseNfAttrTLV(reader) , parseNfAttrTLV 相比 parseNfAttrTL 就是多返回了一个对应读取内容的值.由于本质上还是调用的 parseNfAttrTL ,所以分析方法与上面的分析一致,这里就不做说明了. 此时i已经变为了16.当执行完毕, isNested, attrType, len = parseNfAttrTL(r) 之后,得到 isNested 为false, attrType 为1, len 为4.接下来就是执行:

value = make([]byte, len)
binary.Read(r, binary.BigEndian, &value)
return isNested, attrType, len, value

此时reader继续读取数据,由于len为4,所以就会读取4个元素.读取到value中.

INnIfqZ.png!web

读取方式是大端方式读取.value的值是127.0.0.1. 所以当执行完毕数据,以下的值分别变为:

  • isNested false
  • attrType 1
  • len 4
  • value 127.0.0.1
  • reader的index为20

最后程序回到如下的代码:

const (
	CTA_IP_V4_SRC = 1
	CTA_IP_V4_DST = 2
	CTA_IP_V6_SRC = 3
	CTA_IP_V6_DST = 4
)

for i := 0; i < 2; i++ {
    _, t, _, v := parseNfAttrTLV(reader)
    switch t {
    case CTA_IP_V4_SRC, CTA_IP_V6_SRC:
        tpl.SrcIP = v
    case CTA_IP_V4_DST, CTA_IP_V6_DST:
        tpl.DstIP = v
    }
}

由于t为1,所以命中 CTA_IP_V4_SRC ,最后得到 tpl.SrcIP 为127.0.0.1;同理可以得到 tpl.DstIP 为127.0.0.1. 之后程序又继续执行如下的代码:

// Skip the next 4 bytes  nl.NLA_F_NESTED|nl.CTA_TUPLE_PROTO
reader.Seek(4, seekCurrent)
_, t, _, v := parseNfAttrTLV(reader)
if t == CTA_PROTO_NUM {
    tpl.Protocol = uint8(v[0])
}

const (
	CTA_PROTO_NUM      = 1
	CTA_PROTO_SRC_PORT = 2
	CTA_PROTO_DST_PORT = 3
)

首先会跳过4个字符,之后同样是调用 parseNfAttrTLV 函数.此时data的读取情况如下所示:

rERrUbq.png!web

此时的数据情况是:

  • t:1
  • v:6

最终执行 tpl.Protocol = uint8(v[0]) , 得到 tpl.Protocol 为6,即当前协议是一个tcp的协议.

接下来程序执行如下代码:

// Skip some padding 3 bytes
reader.Seek(3, seekCurrent)
for i := 0; i < 2; i++ {
	_, t, _ := parseNfAttrTL(reader)
	switch t {
	case CTA_PROTO_SRC_PORT:
		parseBERaw16(reader, &tpl.SrcPort)
	case CTA_PROTO_DST_PORT:
		parseBERaw16(reader, &tpl.DstPort)
	}
	// Skip some padding 2 byte
	reader.Seek(2, seekCurrent)
}

程序首先会跳过3个元素,之后同样是调用 parseNfAttrTL(reader) 方法,获取t. 分析方法同上,最终得到的t为2.进入到 parseBERaw16(reader, &tpl.SrcPort) .

func parseBERaw16(r *bytes.Reader, v *uint16) {
	binary.Read(r, binary.BigEndian, v)
}

所以SrcPort的值是 10011101 (157的二进制) + 10000000 (128的二进制) = 1001110110000000(40320). 所以SrcPort是40320.

ARbeqqM.png!web

接下来同样是获取 DstPort 的代码.分析方法一样,最终的结果是: 1000000(4的二进制,补充0000)+111000(56的二进制)= 10000111000(1080),所以DstPort是1080.此时data的状态如下所示:

ZrQviaQ.png!web

最后由于conntrack会同时记录网络包的发送信息和预期的返回包信息,当前我们仅仅只是分析了 CAT_TUPLE_ORIG ,即网络包的发送信息,接下来就是解析预期的返回包信息,解析方法完全与上述分析方法一样,就不做说明了.下图十分清晰明了地说明了各个数据结构.

3uuqIfR.png!web

通过上面的分析,我们发现其实conntrack的连接跟踪表的数据结构还是相当有规律的.整个的数据结构在之前的代码注释也说明了.

// <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>

整个的数据解析过程也是按照这个来进行的.

总结

通过对conntrack的整个数据解析过程的分析,对conntrack的数据结构加深了理解,同时也方便我们利用conntrack来记录主机中的网络状态信息.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK