如何使用golang实现traceroute
作者:水番丘山
Traceroute 概念
traceroute是一种网络诊断工具,通过traceroute可以诊断出本机到目的地IP之间的路由情况,例如路由跳数、延迟、是否可达等信息。该工具在linux环境下的命令是traceroute
或者tracepath
,在windows下命令是tracert
。
工作原理
traceroute在linux系列的操作系统,默认通过发送UDP请求到目的地IP,UDP的端口使用的是33434到33545之间。除了UDP的协议,可选用ICMP或者TCP(TCP SYN包)。使用33434到33534之间到端口是因为大部分linux系统的该范围内的端口是不可用的。正常情况下如果我们对一个目的地主机发起UDP请求,并且该端口不存在就会直接返回端口或者主机不可大的信息,这样是无法获取到中途的路由节点。此时需要引入一个TTL的概念。
TTL即Time-To-Live,更多的被理解为路由跳数,该值存于IP头,经过路由转发时会将该值减1,当ttl值为0时,路由就会回复一个ICMP消息"Time Exceeded",表示跳数已经达到最大值,无法进行转发。
TTL在ipv4和ipv6头有不同的定义,在ipv4头用8位来存该数值,且命名为“Time to Live”,而在ipv6的头则叫做“Hop Limit”。
不管是Time to Live还是Hop Limit,其实都是相同的逻辑,路由转发一次就减1,并且该值为0时则无法转发。
我们来看一下traceroute的发包过程:
第一步:主机A往目的主机B发送UDP包,包头需要设置TTL=1,并且设置目的端口为33434。
第二步:主机A的最近的路由A收到UDP包以后,将TTL减1,此时TTL=0,路由A就将该包丢弃,并且回复主机A一条ICMP信息:“Time Exceeded”。
第三步:主机A收到ICMP的消息以后即可记录ICMP发送主机的地址,该地址就是路由IP,并且主机A设置TTL=2,再次发送UDP包到目的主机B的33434端口。
第四步:以此类推,直到TTL超过设置的最大值或者收到目的主机返回的消息时停止发包,这样就得到了一个路由地址列表,同时也能拿到发送到路由之间的消息延迟,如果路由超过设定的时间内没有相应,则置该跳数的路由地址为“*”。
traceroute-go代码实现
由于go语言是高级语言,将udp以及tcp的包头都封装完整,无法定制设置ttl。好在golang提供了syscall库,该库提供依稀了linux下的函数调用,因此可以利用该包的方法达到设置ttl的目的。在1.4之前可以使用标准库syscall
,但因为该库已经被弃用,可以使用golang.org/x/sys
库,该库是syscall
的扩展,提供更加丰富的系统调用方法。
有库的支持,我们则需要了解一下C语言的知识,即用C语言发送udp包和接受icmp的信息,因此这里需要涉及到几个函数:
socket
函数,创建一个socke的文件描述,用于发送udp以及接收icmp的消息,golang对应的函数为func Socket(domain, typ, proto int) (fd int, err error)
setsockopt
函数,该函数可以用于设定IP的头信息,我们要设定TTL就是利用该函数,同时该函数可以设定socket的请求或者接收消息的超时时间,golang对应的函数为func SetsockoptInt(fd, level, opt int, value int) (err error)
sendto
函数,用于发送udp消息,golang对应的函数为func Sendto(fd int, p []byte, flags int, to Sockaddr) (err error)
recvfrom
函数,用于接收icmp消息,golang对应的函数为func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error)
函数准备好以后就可以开工编写golang版本的traceroute库了。
首先,创建sendSocket,用于发送UDP包,注意内部的参数 unix.IPPROTO_UDP
表示使用ipv4的udp协议,这个与ipv6协议是有区别的,可以通过命令man socket
查看函数说明,然后创建一个recvSocket的socket文件描述符,用于接收ICMP的消息,这里调用了函数SetsockoptTimeval
,用于设定接收消息的超时时间。
然后在for循环内循环发送udp消息并且接收icmp消息:
代码中SetsockoptInt
函数设定ipv4的头TTL,初始化ttl=1,通过Sendto
函数将消息发送到目的地址和目的端口,这里目的端口从33434开始,会在33434到33534区间内循环。
发送消息以后,通过Recvfrom
接收消息,此时会判断接收消息是否报错,如果报错则直接退出循环并结束traceroute操作;如果没有报错,则需要解析返回的ICMP消息,由于ipv4的Header包头长度最小是20字节,最大是60字节,会出现浮动,因此需要拿到实际的ipv4头长度,这里使用ipv4
库的ParseHeader
函数解析拿到ipv4的包头结构,然后将收到的消息截取ipHeader.Len
长度就得到我们的ICMP消息结构体,拿到ICMP消息结构以后既可以根据Type判定消息类型,由于我们只关注ICMPTypeTimeExceeded
和ICMPTypeDestinationUnreachable
类型的消息,因此其他消息我们都会丢弃,并且如果收到的是ICMPTypeTimeExceeded
,则需要将发送方的地址(路由地址)存下来,并且将ttl+1,然后再次循环发送udp消息到目的地。
如果收到的ICMP消息类型是ICMPTypeDestinationUnreachable
或者ttl超过了最大的ttl设定或者接受的的ICMP消息来自于目的地址,则结束发包,并输出结果。
当然,如果接收到报错的消息,该消息可能是路由不通或者发包超时,因此我们需要将该跳的路由地址设置为“*”,同时判定重试次数,以及是否超过了最大TTL。
最后每次循环都将目的端口值+1,并且超过了最大的端口33534是又从最小端口开始,保障端口范围一直在33434到33534之间。
结果输出:
以下是我们自己的程序结果输出:
以下是系统自带的traceroute输出:
总结
traceroute工具原理不难,但要实现这个过程需要涉及到一些基本知识,如ip的报文组成、udp、icmp协议的一些基本知识,另外就是需要知道路由跳数的基本原理,通过实现这个过程也可以加深这些基础知识,同时是对这些知识的运用。
完整代码已经上传到github,地址为:https://github.com/Kseleven/traceroute-go,欢迎大家star,当然如有纰漏或者讲解不正确的地方,欢迎指正。
参考文献
- ipv4 rfc 791
- ipv6 rfc 2460
- icmp rfc 792
- traceroute rfc 1393
- linux man page-traceroute
- traceroute wiki
- icmp wiki
- golang sys库
到此这篇关于如何使用golang实现traceroute的文章就介绍到这了,更多相关golang实现traceroute内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!