UDP 为什么会 write: connection refused

2022年8月15日 0 条评论 3.01k 次阅读 6 人点赞

一、前言

在某一次写 udp 服务时,发现一个奇特的现象,udp client 在调用 write 方法写入数据时会报 connection refused

对于懂得 socket 编程却不懂网络的人可能认为这根本就不是一个问题。

因为这报错显然就是远端服务对客户端发送的数据进行了拒绝,也许会认为远端服务没有起端口。

有时无知也是一种幸福。但是对于熟悉 TCP/IP 五层协议栈的同学肯定就会想不通,udp 本身就没有面向连接这一说法,它调用write时,对端没有接收大不了就是丢包,那么为何还有 connection refused 这一说法?

二、复现代码

下面有一段代码就可以复现这种现象

udp 客户端 client.go

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	conn, err := net.Dial("udp", "39.96.14.232:9981")
	if err != nil {
		fmt.Println("拨号发生错误:", err.Error())
		return
	}
	defer conn.Close()

	for {
		time.Sleep(time.Second)
		if _, err := conn.Write([]byte("hello")); err != nil {
			fmt.Println("写数据发生错误:", err.Error())
			continue
		}
	}
}

udp 服务端 server.go

package main

import (
	"fmt"
	"net"
)

func main() {
	listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("0.0.0.0"), Port: 9981})
	if err != nil {
		fmt.Println(err)
		return
	}
	data := make([]byte, 1024)
	for {
		n, remoteAddr, err := listener.ReadFromUDP(data)
		if err != nil {
			fmt.Printf("读发生错误: %s\n", err)
			continue
		}
		fmt.Printf("从 %s 读取到的内容为 %s \n", remoteAddr, data[:n])
	}
}

当把 server.go ctrl+c 杀死时,我们会发现客户端开始报错,就像下面这样。

三、解析原因

首先我相信公理是不会出错的,既然这里 udp 客户端写入报错,肯定是有其它深层次的原因。

不妨我们用 tcpdump 进行抓包一探究竟

tcpdump -i any  host 39.96.14.232 -s0 -w ./udp.pcap

Wireshark 分析结果如下

从中可以发现,当 udp 客户端发包至服务端时,接收方发现自身对应的 udp 端口没有打开,会通过 icmp 协议回复一个端口不可达的信息。

因此ICMP的包内容就是出错的那个原始数据包,根据这个原始数据包可以找出一个五元组,根据该五元组就可以对应到一个本地的udp socket,进而把错误消息传输给该socket,应用程序在调用socket接口函数的时候,就可以得到该错误消息。

根据这一现象,在一定程度上可以做 udp 端口探测,但这种探测不一定好使,在有些网络环境下 icmp 不可达消息会被过滤掉。

例如当udp服务端所在宿主机对于udp 协议的安全组没开放时,我发现就算无论如何客户端都不会收到该报错。相反,当把 udp 协议的安全组放开时,并关闭udp 服务端时,就会收到接收包回复的 icmp 包现象。

以此记录刚毕业那年遇到的疑问,终于在今日通过实验被解答了。

兰陵美酒郁金香

大道至简 Simplicity is the ultimate form of sophistication.

文章评论(0)

你必须 登录 才能发表评论