最近几周几乎都是单休,加班很多,也遇到了很多未知的问题,杂事也多时间比较紧张,也没有多少空余来进行一些总结积累。这点让我很是怀念起几年前的日子,任务安排周期长,做技术纯粹又专心。
前几天遇到了一个UDP发包无法收到回包的问题,趁着今天不加班来记录回顾一下。
网络环境
两台主机,每台主机上分别有两个网口。
主机1的Ip地址:
- 管理口:192.168.1.2
- 网口一:2006:470:8192:beef:6114:e3d6:a597:aea6
- 网口二:2006:470:8192:beef:d45e:a0fa:9c9c:2e4b
主机2的Ip地址:
- 管理口:192.168.1.3
- 网口一:2005:470:8192:beef:9020:c015:a801:1301
- 网口二:2005:470:8192:beef:68af:d2e6:a0e3:6fb1
如下图:
udpServer
func udpServerStart() {
srcAddr := &net.UDPAddr{
IP: net.IPv6zero,
Port: 12345,
}
//监听不绑定IP
conn, _ := net.ListenUDP("udp6", srcAddr)
fmt.Printf("udp server bind on:[%s]\n", conn.LocalAddr())
for {
buffer := make([]byte, 1024)
//将数据读到buffer中
n, remoteAddr, _ := conn.ReadFromUDP(buffer)
fmt.Printf("remote addr:[%s] \n data:[%v] str:[%s]\n", remoteAddr.String(), buffer[:n], string(buffer[:n]))
//发送回包
_, _ = conn.WriteToUDP([]byte("hello client"), remoteAddr)
}
}
udpClient
再写个httpServer,在"udpTest"接口中实现个udpClient,用来发udp数据包
func httpServerStart() {
udpTestHandler := func(writer http.ResponseWriter, request *http.Request) {
ipStr := request.FormValue("ip")
fmt.Printf("req param,ip:[%s]\n", net.ParseIP(ipStr))
socket, err := net.DialUDP("udp6", nil,
&net.UDPAddr{
IP: net.ParseIP(ipStr),
Port: 12345,
},
)
if err != nil {
fmt.Println("连接UDP服务器失败,err: ", err)
return
}
defer socket.Close()
sendData := []byte("Hello Server")
_, err = socket.Write(sendData) // 发送数据
if err != nil {
fmt.Println("发送数据失败,err: ", err)
return
}
data := make([]byte, 4096)
n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据
if err != nil {
fmt.Println("接收数据失败, err: ", err)
return
}
fmt.Printf("recv:%v addr:%v count:%v\n", string(data[:n]), remoteAddr, n)
}
handler := http.NewServeMux()
handler.Handle("/udpTest", http.TimeoutHandler(http.HandlerFunc(udpTestHandler), time.Second*3, "timeout"))
server := &http.Server{Addr: ":8096", Handler: handler}
err := server.ListenAndServe()
if nil != err {
log.Fatal(err) //显示错误日志
}
}
发包测试
在主机1上向主机2的两个端口发包
http://192.168.1.2:8096/udpTest?ip=2005:470:8192:beef:68af:d2e6:a0e3:6fb1
http://192.168.1.3:8096/udpTest?ip=2005:470:8192:beef:9020:c015:a801:1301
主机1没有日志打印,且http接口都显示超时了。
观察主机2的日志:
udp server bind on:[[::]:12345]
remote addr:[[2003:470:8192:beef:ca43:6a6a:9c4c:b1a0]:48406]
data:[[72 101 108 108 111 32 83 101 114 118 101 114]] str:[Hello Server]
reply addr:[[2003:470:8192:beef:ca43:6a6a:9c4c:b1a0]:48406] length:12
remote addr:[[2003:470:8192:beef:ca43:6a6a:9c4c:b1a0]:35365]
data:[[72 101 108 108 111 32 83 101 114 118 101 114]] str:[Hello Server]
reply addr:[[2003:470:8192:beef:ca43:6a6a:9c4c:b1a0]:35365] length:12
主机2收到了udp包,也发送了回包。
但主机1没有并没有收到,包走丢了?
主机2上用tcpdump抓个包看一下
看到主机2只收到了主机1的udp包,但没有回包发出去,那么回包发到哪去了?
问题定位
因为之前有试过主机1和主机2的tcp和http请求都是通的,但UDP却不通确实感到疑惑。
我们知道http是基于TCP的,UDP和TCP相比也是更加简单的,也无需握手。
查阅了一些资料后发现扯淡的文档居多,直到遇到了这篇好文《Linux路由应用-使用策略路由实现访问控制》,重点片段为:
UDP无连接,不可靠,只负责将数据尽力而为传到目的主机,它对源和目的IP地址的管理很松散,UDP数据流(更确切的并不能称为数据流)是单包的。在两端都没有显式bind到具体的IP地址的情况下,最终的数据包可以使用任意的本机地址,关键看路由的结果。数据到达对端之后,如果对端也没有显示bind到具体的IP地址,那么回复包的源地址也可能不再是初始包的目的地址。
解决办法
其解决办法在《Linux路由应用-使用策略路由实现访问控制》文章中也描述了可以使用配置系统策略路由的方式来解决,这里再总结一下。
方法一:配置策略路由
这部分参考《Linux路由应用-使用策略路由实现访问控制》文章中添加策略路由表部分即可。
方法二:配置系统路由
如果通信时很明确知道数据包是发往某个口的,则可以添加一条系统默认路由来解决。如:
ip -6 route add ::/0 dev ppp0 metric 60
这样当主机二再进行回包时,就会指定使用ppp0口进行回包了。
也可以通过udp进行socket通信时获取到包中的目的地址,再从UDP包中获取出源地址灵活添加系统路由配置。
常见的方式为,可以通过开启IPPROTO_IP进行获取。
如为go语言实现,可通过如下代码开启:
syscall.SetsockoptInt(int(file.Fd()), syscall.IPPROTO_IP, syscall.IP_PKTINFO, 1)
再调用ReadMsgUDP就可以获取到的具体的目的IP了。
详细代码可参考:
udp_ip_pktinfo.go
方法三:udp server绑定具体的IP
当然还有最后一种简单有用的方法,udp server进行监听时对每个网口的ip地址都进行一次绑定,简单方便。