内容简介:在还是以由于是对conntrack的信息进行分析, 因此分析对象就是netfilter的返回包信息.所以我们从
在 用户态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来记录主机中的网络状态信息.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 数据结构 1 线性表详解 链表、 栈 、 队列 结合JAVA 详解
- Runtime数据结构详解
- 数据结构之单链表详解二
- 数据结构之单链表详解一
- 3.10 solidity数据结构详解
- HBase数据结构与基本语法详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design for Hackers
David Kadavy / Wiley / 2011-10-18 / USD 39.99
Discover the techniques behind beautiful design?by deconstructing designs to understand them The term ?hacker? has been redefined to consist of anyone who has an insatiable curiosity as to how thin......一起来看看 《Design for Hackers》 这本书的介绍吧!