内容简介:在还是以由于是对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数据结构与基本语法详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
图解服务器端网络架构
[日] 宫田宽士 / 曾薇薇 / 人民邮电出版社 / 2015-4 / 79.00元
本书以图配文,详细说明了服务器端网络架构的基础技术和设计要点。基础设计是服务器端网络架构最重要的一个阶段。本书就立足于基础设计的设计细分项目,详细介绍各细分项目的相关技术和设计要点。全书共分为5章,分别讲述进行物理设计、逻辑设计、安全设计和负载均衡设计、高可用性设计以及管理设计时所必需的技术和设计要点。一起来看看 《图解服务器端网络架构》 这本书的介绍吧!