conntrack中的数据结构详解
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-go 的 first 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的内容分别如下:
接下来的分析都是基于这个数据,牢记这个数据.
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.
数据的结果也和我们分析情况一致.
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.
parseNfAttrTL
此时reader的index喂,接下来就是执行 parseNfAttrTL(reader)
. 此时的reader情况如下所示:
同样一步步来分析整个过程.
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.如下所示:
此时reader的状态变为:
程序继续执行, 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. 实际分析结果与数据一致.
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的读取状态是:
同样按照上面的分析方法,len长度变为了16.attrType的值是32769.reader移动4为,所以reader的index值是12.执行完毕之后,最终的data的数据情况如下所示:
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中.
读取方式是大端方式读取.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的读取情况如下所示:
此时的数据情况是:
- 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.
接下来同样是获取 DstPort
的代码.分析方法一样,最终的结果是: 1000000(4的二进制,补充0000)+111000(56的二进制)= 10000111000(1080),所以DstPort是1080.此时data的状态如下所示:
最后由于conntrack会同时记录网络包的发送信息和预期的返回包信息,当前我们仅仅只是分析了 CAT_TUPLE_ORIG
,即网络包的发送信息,接下来就是解析预期的返回包信息,解析方法完全与上述分析方法一样,就不做说明了.下图十分清晰明了地说明了各个数据结构.
通过上面的分析,我们发现其实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来记录主机中的网络状态信息.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK